Remote pickup with Asterisk's ChannelRedirect

I recently implemented BLF ring/call indication in my home asterisk setup. And once I had the call indication, I also wanted to be able to pickup a remote call. Of course Asterisk provides ways to achieve this, but those pick up the phone call immediately. At my workplace, the remote pickup feature would make your own phone just ring, giving you a chance to know who's calling and to decide about whether or not you want to take the phone call at all. This is way cooler than directly being connected to god-knows-who. So, I want it.

First of all, here's the extension I used for remote pickup so far:

_*8*6XXZ => {
    PickupChan(SIP/${EXTEN:3});
};

My internal extensions all start with *6, so the pickup extension for *6005 would then be *8*6005 which is what the wildcard is matching. PickupChan then simply picks up the SIP/6005 channel. Straightforward, but picks up immediately, which completely sucks.

A bit of digging in the docs uncovers the ChannelRedirect command which sounds pretty much perfect. So I simply replaced the PickupChan command like this:

_*8*6XXZ => {
    ChannelRedirect(SIP/${EXTEN:3},to-internal,${CALLERID(num)},1);
};

Unfortunately, that's not right: While PickupChan takes the channel being called as an argument, ChannelRedirect expects you to pass in the source channel. But how do you find out the source channel currently calling a completely different extension?

The only way I could think of was storing the caller in the Asterisk database:

hint(SIP/${EXTEN:1}) _*6XXZ => {
    // Store the channel ID in the database for remote pickup
    Set(DB(callerchan/${EXTEN:1})=${CHANNEL});
    Dial(SIP/${EXTEN:1});
};

_*8*6XXZ => {
    // Get the channel ID currently calling the target extension
    Set(TargetCallerChan=${DB(callerchan/${EXTEN:3})});
    ChannelRedirect(${TargetCallerChan},to-internal,${CALLERID(num)},1);
};

This produced the following logs during the initial call:

-- Executing [*6005@from-internal:1] Set("SIP/6004-0000007f", "DB(callerchan/6005)=SIP/6004-0000007f") in new stack
-- Executing [*6005@from-internal:2] Dial("SIP/6004-0000007f", "SIP/6005") in new stack

When calling the pickup extension from another phone, Asterisk had this to say:

-- Executing [*8*6005@from-internal:1] Set("SIP/6001-00000081", "TargetCallerChan=SIP/6004-0000007f") in new stack
-- Executing [*8*6005@from-internal:2] ChannelRedirect("SIP/6001-00000081", "SIP/6004-0000007f,to-internal,SIP/*6001,1") in new stack

So up to this point, everything looks alright — except from the part that this causes the original call to fail and the takeover phone to just do nothing for a couple of seconds, then beep and complain about something having been "declined" without giving any further detail. But when I used the channel redirect console command, everything worked.

The only difference between me entering the command on the command line and dialing the takeover extension is that the takeover phone was completely idle while I typed the command. So maybe we need to un-busy the phone before redirecting the channel?

_*8*6XXZ => {
    Answer();
    SoftHangup();
    Set(TargetCallerChan=${DB(callerchan/${EXTEN:3})});
    ChannelRedirect(${TargetCallerChan},to-internal,${CALLERID(num)},1);
};

Et voilà, it's working! So the complete code I use now looks like this:

hint(SIP/${EXTEN:1}) _*6XXZ => {
    // Store the channel ID in the database for remote pickup
    Set(DB(callerchan/${EXTEN:1})=${CHANNEL});
    Dial(SIP/${EXTEN:1});
};

_*8*6XXZ => {
    // The caller wants to pickup a call currently active on the remote line,
    // but their channel is currently busy calling *this* extension.
    // Answer and immediately hangup to free the line, then redirect the other channel.
    Answer();
    SoftHangup();
    // Get the channel ID currently calling the target extension
    Set(TargetCallerChan=${DB(callerchan/${EXTEN:3})});
    ChannelRedirect(${TargetCallerChan},to-internal,${CALLERID(num)},1);
};