rockbox/apps/iap/iap-lingo2.c
Le Khanh Binh d00a53a1f0 iap: Fix dead remote at boot & ghost clicks on held buttons
Fix FS#13832
If the remote is already plugged in when Rockbox boots, it stays dead
until you replug it. These remotes only send their identify packet
once at power-up, so we never see it and then throw away all button
events because lingo 0x02 was never negotiated. The OF doesn't care,
so accept ContextButtonStatus from unidentified devices too. Stray
bytes received before iap_setup() also made iap_getc() return false,
which the serial driver takes as "sync found" and locks autobaud onto
the wrong bitrate.

The ghost clicks come from the repeatbtn handshake. A held button
resends its down event every 30-100ms and each one re-armed
iap_repeatbtn, which stalls handling of the following packet. So
during a long press packets pile up, the event queue overflows, and
the leftovers replay as keypresses after you let go. Skipping fires
on button release which is why next/prev showed it the worst. Now
repeatbtn is only re-armed when the button state actually changes,
and iap_handlepkt() drains whatever complete packets are in the RX
buffer rather than relying on one queued event per packet.

While at it, fix two more things found along the way: the button
state is now written in one go (the temporary BUTTON_NONE could be
picked up by the button tick mid-update), and iap_rxlen is
decremented when a frame is received so the overflow check actually
does something.

Tested on ipodmini2g with an A1018 remote.

Change-Id: Ie18512e47d642fe6957fc010eaf79b6d2af4c070
2026-06-16 21:36:02 +07:00

375 lines
12 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* Copyright (C) 2002 by Alan Korr & Nick Robinson
*
* All files in this archive are subject to the GNU General Public License.
* See the file COPYING in the source tree root for full license agreement.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
/* Lingo 0x02, Simple Remote Lingo
*
* TODO:
* - Fix cmd 0x00 handling, there has to be a more elegant way of doing
* this
*/
#include "iap-core.h"
#include "iap-lingo.h"
#include "system.h"
#include "button.h"
#include "audio.h"
#include "settings.h"
#include "tuner.h"
#if CONFIG_TUNER
#include "ipod_remote_tuner.h"
#endif
/*
* This macro is meant to be used inside an IAP mode message handler.
* It is passed the expected minimum length of the message buffer.
* If the buffer does not have the required lenght an ACK
* packet with a Bad Parameter error is generated.
*/
#define CHECKLEN(x) do { \
if (len < (x)) { \
cmd_ack(cmd, IAP_ACK_BAD_PARAM); \
return; \
}} while(0)
static void cmd_ack(const unsigned char cmd, const unsigned char status)
{
IAP_TX_INIT(0x02, 0x01);
IAP_TX_PUT(status);
IAP_TX_PUT(cmd);
iap_send_tx();
}
#define cmd_ok(cmd) cmd_ack((cmd), IAP_ACK_OK)
void iap_handlepkt_mode2(const unsigned int len, const unsigned char *buf)
{
static bool poweron_pressed = false;
#if CONFIG_TUNER
static bool remote_mute = false;
#endif
unsigned int cmd = buf[1];
/* We expect at least three bytes in the buffer, one for the
* lingo, one for the command, and one for the first button
* state bits.
*/
CHECKLEN(3);
/* Lingo 0x02 must have been negotiated, except for
* ContextButtonStatus (0x00): simple remotes like the Apple A1018
* identify only once at power-up. If the remote was already
* powered before Rockbox started (e.g. plugged in at boot) that
* identification is never seen, and rejecting the button events
* would leave the remote dead until it is replugged. The OF
* accepts these events without identification, so do the same.
*/
if ((cmd != 0x00) && !DEVICE_LINGO_SUPPORTED(0x02)) {
cmd_ack(cmd, IAP_ACK_BAD_PARAM);
return;
}
switch (cmd)
{
/* ContextButtonStatus (0x00)
*
* Transmit button events from the device to the iPod
*
* Packet format (offset in buf[]: Description)
* 0x00: Lingo ID: Simple Remote Lingo, always 0x02
* 0x01: Command, always 0x00
* 0x02: Button states 0:7
* 0x03: Button states 8:15 (optional)
* 0x04: Button states 16:23 (optional)
* 0x05: Button states 24:31 (optional)
*
* Returns: (none)
*/
case 0x00:
{
unsigned long btn = BUTTON_NONE;
if(buf[2] != 0)
{
if(buf[2] & 1)
{
btn |= BUTTON_RC_PLAY;
#if CONFIG_TUNER
if (radio_present == 1) {
if (remote_mute == 0) {
/* Not Muted so radio on*/
tuner_set(RADIO_MUTE,0);
} else {
/* Muted so radio off*/
tuner_set(RADIO_MUTE,1);
}
remote_mute = !remote_mute;
}
#endif
}
if(buf[2] & 2)
btn |= BUTTON_RC_VOL_UP;
if(buf[2] & 4)
btn |= BUTTON_RC_VOL_DOWN;
if(buf[2] & 8)
btn |= BUTTON_RC_RIGHT;
if(buf[2] & 16)
btn |= BUTTON_RC_LEFT;
}
else if(len >= 4 && buf[3] != 0)
{
if(buf[3] & 1) /* play */
{
if (audio_status() != AUDIO_STATUS_PLAY)
btn |= BUTTON_RC_PLAY;
#if CONFIG_TUNER
if (radio_present == 1) {
tuner_set(RADIO_MUTE,0);
}
#endif
}
if(buf[3] & 2) /* pause */
{
if (audio_status() == AUDIO_STATUS_PLAY)
btn |= BUTTON_RC_PLAY;
#if CONFIG_TUNER
if (radio_present == 1) {
tuner_set(RADIO_MUTE,1);
}
#endif
}
if(buf[3] & 128) /* Shuffle */
{
if (!iap_btnshuffle)
{
iap_shuffle_state(!global_settings.playlist_shuffle);
iap_btnshuffle = true;
}
}
}
else if(len >= 5 && buf[4] != 0)
{
if(buf[4] & 1) /* repeat */
{
if (!iap_btnrepeat)
{
iap_repeat_next();
iap_btnrepeat = true;
}
}
if (buf[4] & 2) /* power on */
{
poweron_pressed = true;
}
/* Power off
* Not quite sure how to react to this, but stopping playback
* is a good start.
*/
if (buf[4] & 0x04)
{
if (audio_status() == AUDIO_STATUS_PLAY)
btn |= BUTTON_RC_PLAY;
}
if(buf[4] & 16) /* ffwd */
btn |= BUTTON_RC_RIGHT;
if(buf[4] & 32) /* frwd */
btn |= BUTTON_RC_LEFT;
if(buf[4] & 64) /* menu */
btn |= BUTTON_RC_MENU;
if(buf[4] & 128) /* select */
btn |= BUTTON_RC_SELECT;
}
else if(len >= 6 && buf[5] != 0)
{
if(buf[5] & 1) /* up */
btn |= BUTTON_RC_UP;
if (buf[5] & 2) /* down */
btn |= BUTTON_RC_DOWN;
}
/* Commit the new button state in one go. The button driver
* reads iap_remotebtn from interrupt context, a transient
* BUTTON_NONE between two repeated button-down events would
* register as a spurious release/press pair.
*/
if (btn != BUTTON_NONE)
{
/* Only re-arm iap_repeatbtn when the state changes. A
* held button repeats its down event every 30-100ms,
* re-arming on every repeat makes iap_handlepkt()
* delay each following packet, so packets back up
* during the hold and stale events replay as phantom
* keypresses after the release.
*/
if (btn != iap_remotebtn)
iap_repeatbtn = 2;
iap_remotebtn = btn;
iap_timeoutbtn = 3;
}
else
{
iap_remotebtn = BUTTON_NONE;
iap_timeoutbtn = 0;
}
/* power on released */
if (poweron_pressed && len >= 5 && !(buf[4] & 2))
{
poweron_pressed = false;
#ifdef HAVE_LINE_REC
/* Belkin TuneTalk microphone sends power-on press+release
* events once authentication sequence is finished,
* GetDevCaps command is ignored by the device when it is
* sent before power-on release event is received.
* XXX: It is unknown if other microphone devices are
* sending the power-on events.
*/
if (DEVICE_LINGO_SUPPORTED(0x01)) {
/* GetDevCaps */
IAP_TX_INIT(0x01, 0x07);
iap_send_tx();
}
#endif
}
break;
}
/* ACK (0x01)
*
* Sent from the iPod to the device
*/
/* ImageButtonStatus (0x02)
*
* Transmit image button events from the device to the iPod
*
* Packet format (offset in buf[]: Description)
* 0x00: Lingo ID: Simple Remote Lingo, always 0x02
* 0x01: Command, always 0x02
* 0x02: Button states 0:7
* 0x03: Button states 8:15 (optional)
* 0x04: Button states 16:23 (optional)
* 0x05: Button states 24:31 (optional)
*
* This command requires authentication
*
* Returns on success:
* IAP_ACK_OK
*
* Returns on failure:
* IAP_ACK_*
*/
case 0x02:
{
if (!DEVICE_AUTHENTICATED) {
cmd_ack(cmd, IAP_ACK_NO_AUTHEN);
break;
}
cmd_ack(cmd, IAP_ACK_CMD_FAILED);
break;
}
/* VideoButtonStatus (0x03)
*
* Transmit video button events from the device to the iPod
*
* Packet format (offset in buf[]: Description)
* 0x00: Lingo ID: Simple Remote Lingo, always 0x02
* 0x01: Command, always 0x03
* 0x02: Button states 0:7
* 0x03: Button states 8:15 (optional)
* 0x04: Button states 16:23 (optional)
* 0x05: Button states 24:31 (optional)
*
* This command requires authentication
*
* Returns on success:
* IAP_ACK_OK
*
* Returns on failure:
* IAP_ACK_*
*/
case 0x03:
{
if (!DEVICE_AUTHENTICATED) {
cmd_ack(cmd, IAP_ACK_NO_AUTHEN);
break;
}
cmd_ack(cmd, IAP_ACK_CMD_FAILED);
break;
}
/* AudioButtonStatus (0x04)
*
* Transmit audio button events from the device to the iPod
*
* Packet format (offset in buf[]: Description)
* 0x00: Lingo ID: Simple Remote Lingo, always 0x02
* 0x01: Command, always 0x04
* 0x02: Button states 0:7
* 0x03: Button states 8:15 (optional)
* 0x04: Button states 16:23 (optional)
* 0x05: Button states 24:31 (optional)
*
* This command requires authentication
*
* Returns on success:
* IAP_ACK_OK
*
* Returns on failure:
* IAP_ACK_*
*/
case 0x04:
{
unsigned char repeatbuf[6];
if (!DEVICE_AUTHENTICATED) {
cmd_ack(cmd, IAP_ACK_NO_AUTHEN);
break;
}
/* This is basically the same command as ContextButtonStatus (0x00),
* with the difference that it requires authentication and that
* it returns an ACK packet to the device.
* So just route it through the handler again, with 0x00 as the
* command
*/
memcpy(repeatbuf, buf, 6);
repeatbuf[1] = 0x00;
iap_handlepkt_mode2((len<6)?len:6, repeatbuf);
cmd_ok(cmd);
break;
}
/* The default response is IAP_ACK_BAD_PARAM */
default:
{
#ifdef LOGF_ENABLE
logf("iap: Unsupported Mode02 Command");
#endif
cmd_ack(cmd, IAP_ACK_BAD_PARAM);
break;
}
}
}