Transfer by DTMF dials immediately on first matched extension.

Good day,

We have recently extended our Asterisk system, and I am experiencing an issue with call transfers by DTMF (features.conf).

All the basics work perfectly, but when dialling the target number, Asterisk immediately starts dialling after the first digit (which matches some legacy quick access codes). This makes it impossible to enter a 4 digit extension.

A simple edit to ast_app_dtget() in main/app.c allows me to transfer to an extension, but I am sure there must be a better way than this:

        for (x = strlen(collect); x < maxlen; ) {
                res = ast_waitfordigit(chan, timeout);
                if (!ast_ignore_pattern(context, collect)) {
                        ast_playtones_stop(chan);
                }
                if (res < 1) {
                        break;
                }
                if (res == '#') {
                        break;
                }
                collect[x++] = res;
                //if (!ast_matchmore_extension(chan, context, collect, 1,
                //      S_COR(ast_channel_caller(chan)->id.number.valid, ast_channel_caller(chan)->id.number.str, NULL))) {
                //      break;
                //}
        }


(At first glance the commented out code looks like it is supposed to exit when it is no longer a valid extension, but in fact it exits the moment a valid extension is detected, which does not match the documented behaviour of E_MATCHMORE.)

Thanks,

Justin

I haven’t seen any issue reports for such a thing, so I’d suggest providing the dialplan in question, state which type of transfer you are doing, as well as console output.

That sounds like a badly designed number plan, possibly combined with a bad choice of wildcard character. You may also have set a short digit timeout.

I’ve never worked with a numbering plan where length is critical, in that way, so I’m not really sure how WaitExten would handle it. When someone dials an extension normally, the phone decides when to forward the number, so Asterisk will have more than the short code available, when it fisrt sees any of the number, so won’t normally match it.

This is an attended transfer.

It seems new users can’t upload files, so I will need to quote them here. The dialplan is indeed a mess, with 20 years of history lugged along…

[general]
static=yes
writeprotect=yes
autofallthrough=yes
extenpatternmatchnew=no
clearglobalvars=yes

[globals]
; General internal dialing options used in context Dial-Users.
; Only the timeout is defined here. See the Dial app documentation for
; additional options.

[no-op]
; just hang up
exten => s,1,Hangup(21)

[Features]
; Extension to check user voicemail. We don't requre the user to enter
; their pincode.
exten = 8000,1,Verbose(1,"User ${CALLERID(num)} dialed the voicemail feature.")
 same = n,VoiceMailMain(${CALLERID(num)}@default,s)
 same = n,Hangup()

exten = _X!,1,Return

[Dialing-Errors]
; Handle any extensions dialed internally that don't otherwise exist.
; Comment out or remove this extension if you would rather have the calls
; ignored.
exten = _X!,1,Verbose(1,"Dialing-Errors: User ${CALLERID(num)} dialed an invalid number.")
 same = n,Playback(pbx-invalid)
 same = n,Hangup()

[Internal-Setup]
; Here we capture internal calls to do anything we need to do before sending
; them onto all the possible internal locations. Such as disabling CDR on
; internal to internal calls.
exten = _X!,1,NoOp()
; same = n,Set(CDR_PROP(disable)=1)
 same = n,Return

; Dial-Users handles calls to internal extensions.
; Calls coming into this context may be *external* or *internal* in origin.
[Dial-Users]
exten = _X!,1,Verbose(1,"Dial Users: User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Dial-Users-Rules,${EXTEN},1)
 same = n,Verbose(1,"Dial Users (did not accept): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Return

[Dial-Users-Rules]
exten = _X!,1,Verbose(1,"Dial Users-Rules (NOMATCH - RETURN): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Return

;single digit codes...
exten =_[0-7],1,Goto(Dial-Users-Rules,087xxxxxx${EXTEN},1)
exten = _X,1,Goto(Dialing-Errors,${EXTEN},1)

; 4 digit codes
exten=_5878,1,Goto(Dial-Users-Rules,087xxxxxx4,1)
exten=_4284,1,Goto(Dial-Users-Rules,087xxxxxx3,1)
exten=_2474,1,Goto(Dial-Users-Rules,087xxxxxx6,1)
exten=_5226,1,Goto(Dial-Users-Rules,087xxxxxx6,1)
exten=_6384,1,Goto(Dial-Users-Rules,087xxxxxx6,1)
exten=_9258,1,Goto(Dial-Users-Rules,087xxxxxx2,1)
exten=_8432,1,Goto(Dial-Users-Rules,087xxxxxx1,1)
exten=_3662,1,Goto(Dial-Users-Rules,087xxxxxx1,1)
exten=_2627,1,Goto(Dial-Users-Rules,087xxxxxx5,1)
exten = _XXXX,1,Goto(Dialing-Errors,${EXTEN},1)

; 5 digit codes (cell numbers)
exten=_58780,1,Goto(Outbound-Dial,082xxxxxxx,1)
exten=_42840,1,Goto(Outbound-Dial,073xxxxxxx,1)
;exten=_24740,1,Goto(Outbound-Dial,087xxxxxx6,1)
exten=_52260,1,Goto(Outbound-Dial,083xxxxxxx,1)
;exten=_63840,1,Goto(Outbound-Dial,087xxxxxx6,1)
exten=_92580,1,Goto(Outbound-Dial,082xxxxxxx,1)
;exten=_84320,1,Goto(Outbound-Dial,087xxxxxx1,1)
;exten=_36620,1,Goto(Outbound-Dial,087xxxxxx1,1)
exten = _XXXXX,1,Goto(Dialing-Errors,${EXTEN},1)

exten = 087xxxxxx1,1,Set(SAC_DIALED_EXTEN=donald)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/donald)
 same = n,Goto(sip3,1)
exten = 087xxxxxx2,1,Set(SAC_DIALED_EXTEN=walter)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/walter)
 same = n,Set(DIAL2=PJSIP/walter2)
 same = n,Goto(sip3,1)
exten = 087xxxxxx3,1,Set(SAC_DIALED_EXTEN=gavin)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/gavin2)
 same = n,Set(DIAL2=PJSIP/gavin)
 same = n,Goto(sip3,1)
exten = 087xxxxxx4,1,Set(SAC_DIALED_EXTEN=justin)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/justin)
 same = n,Set(DIAL2=PJSIP/justin2)
 same = n,Goto(sip3,1)
exten = 0871383009,1,Set(SAC_DIALED_EXTEN=justin)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/justin)
 same = n,Set(DIAL2=PJSIP/justin2)
 same = n,Goto(sip3,1)
exten = 087xxxxxx5,1,Set(SAC_DIALED_EXTEN=boardroom)
 same = n,Set(SAC_VM_EXTEN=012xxxxxxx)
 same = n,Set(DIAL1=PJSIP/boardroom)
 same = n,Goto(sip3,1)
exten = 087xxxxxx6,1,Set(SAC_DIALED_EXTEN=junior)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(DIAL1=PJSIP/junior)
 same = n,Goto(sip3,1)
exten = 087xxxxxx7,1,Set(SAC_DIALED_EXTEN=APM)
 same = n,Ringing()
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Verbose(1, "call apm agi")
 same = n,AGI(apm.agi)
 same = n,Verbose(1, "apm agi done 1")

exten = sip3,1,Verbose(1,"Dial-Users (SIP3): User ${CALLERID(num)} dialed ${EXTEN}: ${SAC_DIALED_EXTEN} ${DIAL1} ${DIAL2} ${DIAL3}.")
 same = n,Set(__TRANSFER_CONTEXT=Long-Distance)
 same = n,Verbose(1,"${DIAL1} - ${DEVICE_STATE(${DIAL1})}")
 same = n,Set(DIALALL=)
 same = n,Gotoif($[${DEVICE_STATE(${DIAL1})} != NOT_INUSE]?dial2)
 same = n,Set(DIALALL=${DIALALL}&${DIAL1})
 same = n(dial2),Verbose(1,"${DIAL2} - ${DEVICE_STATE(${DIAL2})}")
 same = n,Gotoif($[${DEVICE_STATE(${DIAL2})} != NOT_INUSE]?dial3)
 same = n,Set(DIALALL=${DIALALL}&${DIAL2})
 same = n(dial3),Verbose(1,"${DIAL3} - ${DEVICE_STATE(${DIAL3})}")
 same = n,Gotoif($[${DEVICE_STATE(${DIAL3})} != NOT_INUSE]?dialx)
 same = n,Set(DIALALL=${DIALALL}&${DIAL3})
 same = n(dialx),Verbose(1,"DIALALL: ${DIALALL:1}")
 same = n,Gotoif(${DIALALL}?:dialed-NOANSWER,1)
 same = n,Dial(${DIALALL:1},30,tx)
 same = n,Goto(dialed-NOANSWER,1)

; remainder go to reception
exten = _087xxxxxx[0-7],1,Verbose(1,"User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(SAC_DIALED_EXTEN=${EXTEN})
 same = n,Goto(012xxxxxxx,4)

exten = 012xxxxxxx,1,Verbose(1,"User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Set(__TRANSFER_CONTEXT=Long-Distance)
 same = n,Set(SAC_VM_EXTEN=${EXTEN})
 same = n,Set(SAC_DIALED_EXTEN=${EXTEN})
 same = n,Dial(PJSIP/walter2&PJSIP/gavin&PJSIP/justin&PJSIP/junior&PJSIP/donald&PJSIP/boardroom,30,tx)
 same = n,Dial(PJSIP/donald,15,tx)
 same = n,Dial(PJSIP/walter2&PJSIP/gavin&PJSIP/justin&PJSIP/junior,30,tx)
 same = n,Voicemail(${SAC_VM_EXTEN}@default,u)
 same = n,Hangup()

exten = dialed-NOANSWER,1,NoOp()
 same = n,Voicemail(${SAC_VM_EXTEN}@default,u)
 same = n,Hangup()

exten = dialed-BUSY,1,NoOp()
 same = n,Voicemail(${SAC_VM_EXTEN}@default,b)
 same = n,Hangup()

exten = dialed-CHANUNAVAIL,1,NoOp()
 same = n,Playback(pbx-invalid)
 same = n,Hangup()

exten = _dialed-.,1,Goto(dialed-NOANSWER,1)

;exten = h,1,Hangup()
exten = h,1,Verbose(1, "CALL HUNG UP")

; Callers in the directory may dial 0 which will jump to the
; 'o' extension.
;exten = o,1,Goto(1111)

; Outbound-Dial
;
; Before we dial, see if the extension matches our restricted number patterns.
; Note that this is a basic set of numbers which could incur a fee if dialed.
; The NANP includes many other numbers that you may want to block. If you feel
; it is necessary to block further number patterns you'll have to add them below
; or you may consider implementing a blacklist via methods beyond the scope of
; this example.
[Outbound-Dial]
exten = _X!,1,Verbose(1,"Outboud-Dial: User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Outbound-Dial-Rules,${EXTEN},1)
 same = n,Verbose(1,"Outboud-Dial (RULES RETURNED???): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Return

[Outbound-Dial-Rules]
; Dial outbound through our SIP ITSP.
exten = _XXXXXXXXXX!,1,Verbose(1,"Didn't match any restricted numbers, proceeding with outbound dial.")
 same = n,Set(__TRANSFER_CONTEXT=Long-Distance)
 same = n,Set(CALLERID(num)=2712xxxxxxx)
; same = n,Dial(PJSIP/${EXTEN}@bitco,,TX)
 same = n,Dial(PJSIP/${EXTEN}@bitconew,,TX)
 same = n,Goto(s-${DIALSTATUS},1)

exten => s-CANCEL,1,Hangup
exten => s-NOANSWER,n,GotoIf($["${DTIME}" = "0"]?4)
exten => s-NOANSWER,n,Hangup
exten => s-BUSY,1,Playtones(busy)
 same => n,Wait(5)
 same => n,Busy
exten => s-CHANUNAVAIL,1,Goto(s-BUSY,1)
exten => s-CONGESTION,1,Playtones(congestion)
 same => n,Wait(5)
 same => n,Congestion
exten => _s-.,1,Goto(s-CONGESTION,1)
exten => s-,1,Goto(s-CONGESTION,1)

; Calls from internal endpoints will enter into one of the two following
; contexts based on their dialing privilege.
[Local]
exten = _X!,1,Verbose(1,"Local: User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Internal-Setup,${EXTEN},1)
 same = n,Verbose(1,"Local (after Internal-Setup): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Features,${EXTEN},1)
 same = n,Verbose(1,"Local (after Features): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Dial-Users,${EXTEN},1)
 same = n,Verbose(1,"Local (after Dial-Users): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Return


;exten = 03XX,1,Goto(Dial-Users,${EXTEN},1)
;exten = _XXXX,1,Goto(Features,${EXTEN},1)

; pre-clean local extens
[Long-Distance]
exten = _+27X!,1,Goto(Long-Distance-Rules,0${EXTEN:3},1)
exten = _+X!,1,Goto(Long-Distance-Rules,${EXTEN:1},1)
exten = _X!,1,Goto(Long-Distance-Rules,${EXTEN},1)

[Long-Distance-Rules]
exten = _X!,1,Verbose(1,"Long-Distance: User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Local,${EXTEN},1)
 same = n,Verbose(1,"Long-Distance (after local): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(Outbound-Dial,${EXTEN},1)
 same = n,Verbose(1,"Long-Distance (after external NO MORE HANDLERS - ERROR): User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Goto(Dialing-Errors)

; The DID-Extensions context captures inbound calls from our ITSP that have a
; DID number where the last four digits matches an internal extension.
[DID-Extensions]
exten = 012xxxxxxx,1,Verbose(1,"External caller dialed inbound to DID ${EXTEN})")
 same = n,Gosub(Dial-Users,${EXTEN},1)
 same = n,Goto(Dialing-Errors)
exten = _087xxxxxx[0-7],1,Verbose(1,"External caller dialed inbound to DID ${EXTEN})")
 same = n,Gosub(Dial-Users,${EXTEN},1)
 same = n,Goto(Dialing-Errors)

exten = _X,1,Verbose(1,"External caller dialed inbound to DID ${EXTEN} (INVALID!)")
 same = n,Return

[bitco]
exten = _+27X!,1,Goto(bitco-Rules,0${EXTEN:3},1)
exten = _+X!,1,Goto(bitco-Rules,${EXTEN:1},1)
exten = _X!,1,Goto(bitco-Rules,${EXTEN},1)

exten = s,1,Hangup

[bitconew]
exten = _+27X!,1,Goto(bitco-Rules,0${EXTEN:3},1)
exten = _+X!,1,Goto(bitco-Rules,${EXTEN:1},1)
exten = _X!,1,Goto(bitco-Rules,${EXTEN},1)

exten = s,1,Hangup

[bitco-Rules]
exten = _X!,1,Verbose(1,"BITCO: User ${CALLERID(num)} dialed ${EXTEN}.")
 same = n,Gosub(DID-Extensions,${EXTEN},1)
 same = n,Verbose(1,"BITCO: User ${CALLERID(num)} dialed ${EXTEN} (INVALID).")
 same = n,Goto(Dialing-Errors)
Connected to Asterisk 22.6.0 currently running on vmserver (pid = 29540)
    -- Executing [5878@Long-Distance:1] Goto("PJSIP/gavin-00000000", "Long-Distance-Rules,5878,1") in new stack
    -- Goto (Long-Distance-Rules,5878,1)
    -- Executing [5878@Long-Distance-Rules:1] Verbose("PJSIP/gavin-00000000", "1,"Long-Distance: User 087xxxxxx3 dialed 5878."") in new stack
 "Long-Distance: User 087xxxxxx3 dialed 5878."
    -- Executing [5878@Long-Distance-Rules:2] Gosub("PJSIP/gavin-00000000", "Local,5878,1") in new stack
    -- Executing [5878@Local:1] Verbose("PJSIP/gavin-00000000", "1,"Local: User 087xxxxxx3 dialed 5878."") in new stack
 "Local: User 087xxxxxx3 dialed 5878."
    -- Executing [5878@Local:2] Gosub("PJSIP/gavin-00000000", "Internal-Setup,5878,1") in new stack
    -- Executing [5878@Internal-Setup:1] NoOp("PJSIP/gavin-00000000", "") in new stack
    -- Executing [5878@Internal-Setup:2] Return("PJSIP/gavin-00000000", "") in new stack
    -- Executing [5878@Local:3] Verbose("PJSIP/gavin-00000000", "1,"Local (after Internal-Setup): User 087xxxxxx3 dialed 5878."") in new stack
 "Local (after Internal-Setup): User 087xxxxxx3 dialed 5878."
    -- Executing [5878@Local:4] Gosub("PJSIP/gavin-00000000", "Features,5878,1") in new stack
    -- Executing [5878@Features:1] Return("PJSIP/gavin-00000000", "") in new stack
    -- Executing [5878@Local:5] Verbose("PJSIP/gavin-00000000", "1,"Local (after Features): User 087xxxxxx3 dialed 5878."") in new stack
 "Local (after Features): User 087xxxxxx3 dialed 5878."
    -- Executing [5878@Local:6] Gosub("PJSIP/gavin-00000000", "Dial-Users,5878,1") in new stack
    -- Executing [5878@Dial-Users:1] Verbose("PJSIP/gavin-00000000", "1,"Dial Users: User 087xxxxxx3 dialed 5878."") in new stack
 "Dial Users: User 087xxxxxx3 dialed 5878."
    -- Executing [5878@Dial-Users:2] Gosub("PJSIP/gavin-00000000", "Dial-Users-Rules,5878,1") in new stack
    -- Executing [5878@Dial-Users-Rules:1] Goto("PJSIP/gavin-00000000", "Dial-Users-Rules,087xxxxxx4,1") in new stack
    -- Goto (Dial-Users-Rules,087xxxxxx4,1)
    -- Executing [087xxxxxx4@Dial-Users-Rules:1] Set("PJSIP/gavin-00000000", "SAC_DIALED_EXTEN=justin") in new stack
    -- Executing [087xxxxxx4@Dial-Users-Rules:2] Set("PJSIP/gavin-00000000", "SAC_VM_EXTEN=087xxxxxx4") in new stack
    -- Executing [087xxxxxx4@Dial-Users-Rules:3] Set("PJSIP/gavin-00000000", "DIAL1=PJSIP/justin") in new stack
    -- Executing [087xxxxxx4@Dial-Users-Rules:4] Set("PJSIP/gavin-00000000", "DIAL2=PJSIP/justin2") in new stack
    -- Executing [087xxxxxx4@Dial-Users-Rules:5] Goto("PJSIP/gavin-00000000", "sip3,1") in new stack
    -- Goto (Dial-Users-Rules,sip3,1)
    -- Executing [sip3@Dial-Users-Rules:1] Verbose("PJSIP/gavin-00000000", "1,"Dial-Users (SIP3): User 087xxxxxx3 dialed sip3: justin PJSIP/justin PJSIP/justin2 ."") in new stack
 "Dial-Users (SIP3): User 087xxxxxx3 dialed sip3: justin PJSIP/justin PJSIP/justin2 ."
    -- Executing [sip3@Dial-Users-Rules:2] Set("PJSIP/gavin-00000000", "__TRANSFER_CONTEXT=Long-Distance") in new stack
    -- Executing [sip3@Dial-Users-Rules:3] Verbose("PJSIP/gavin-00000000", "1,"PJSIP/justin - NOT_INUSE"") in new stack
 "PJSIP/justin - NOT_INUSE"
    -- Executing [sip3@Dial-Users-Rules:4] Set("PJSIP/gavin-00000000", "DIALALL=") in new stack
    -- Executing [sip3@Dial-Users-Rules:5] GotoIf("PJSIP/gavin-00000000", "0?dial2") in new stack
    -- Executing [sip3@Dial-Users-Rules:6] Set("PJSIP/gavin-00000000", "DIALALL=&PJSIP/justin") in new stack
    -- Executing [sip3@Dial-Users-Rules:7] Verbose("PJSIP/gavin-00000000", "1,"PJSIP/justin2 - UNAVAILABLE"") in new stack
 "PJSIP/justin2 - UNAVAILABLE"
    -- Executing [sip3@Dial-Users-Rules:8] GotoIf("PJSIP/gavin-00000000", "1?dial3") in new stack
    -- Goto (Dial-Users-Rules,sip3,10)
    -- Executing [sip3@Dial-Users-Rules:10] Verbose("PJSIP/gavin-00000000", "1," - INVALID"") in new stack
 " - INVALID"
    -- Executing [sip3@Dial-Users-Rules:11] GotoIf("PJSIP/gavin-00000000", "1?dialx") in new stack
    -- Goto (Dial-Users-Rules,sip3,13)
    -- Executing [sip3@Dial-Users-Rules:13] Verbose("PJSIP/gavin-00000000", "1,"DIALALL: PJSIP/justin"") in new stack
 "DIALALL: PJSIP/justin"
    -- Executing [sip3@Dial-Users-Rules:14] GotoIf("PJSIP/gavin-00000000", "&PJSIP/justin?:dialed-NOANSWER,1") in new stack
    -- Executing [sip3@Dial-Users-Rules:15] Dial("PJSIP/gavin-00000000", "PJSIP/justin,30,tx") in new stack
    -- Called PJSIP/justin
    -- PJSIP/justin-00000001 is ringing
       > 0x7f5b14110cd0 -- Strict RTP learning after remote address set to: 192.168.1.128:5004
    -- PJSIP/justin-00000001 answered PJSIP/gavin-00000000
       > 0x7f5b140fa890 -- Strict RTP learning after remote address set to: 192.168.1.152:5050
    -- Channel PJSIP/justin-00000001 joined 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
    -- Channel PJSIP/gavin-00000000 joined 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
       > 0x7f5b14110cd0 -- Strict RTP switching to RTP target address 192.168.1.128:5004 as source
       > 0x7f5b140fa890 -- Strict RTP switching to RTP target address 192.168.1.152:5050 as source
    -- Channel PJSIP/justin-00000001: Started DTMF attended transfer.
    -- Started music on hold, class 'default', on channel 'PJSIP/gavin-00000000'
    -- <PJSIP/justin-00000001> Playing 'pbx-transfer.alaw' (language 'en')
       > 0x7f5b14110cd0 -- Strict RTP learning complete - Locking on source address 192.168.1.128:5004
       > 0x7f5b140fa890 -- Strict RTP learning complete - Locking on source address 192.168.1.152:5050
    -- Executing [5@Long-Distance:1] Goto("Local/5@Long-Distance-00000000;2", "Long-Distance-Rules,5,1") in new stack
    -- Goto (Long-Distance-Rules,5,1)
    -- Executing [5@Long-Distance-Rules:1] Verbose("Local/5@Long-Distance-00000000;2", "1,"Long-Distance: User 087xxxxxx4 dialed 5."") in new stack
 "Long-Distance: User 087xxxxxx4 dialed 5."
    -- Executing [5@Long-Distance-Rules:2] Gosub("Local/5@Long-Distance-00000000;2", "Local,5,1") in new stack
    -- Channel Local/5@Long-Distance-00000000;1 joined 'simple_bridge' basic-bridge <c9533522-de78-4103-a4f0-96b7bf2185d4>
    -- Executing [5@Local:1] Verbose("Local/5@Long-Distance-00000000;2", "1,"Local: User 087xxxxxx4 dialed 5."") in new stack
 "Local: User 087xxxxxx4 dialed 5."
    -- Executing [5@Local:2] Gosub("Local/5@Long-Distance-00000000;2", "Internal-Setup,5,1") in new stack
    -- Executing [5@Internal-Setup:1] NoOp("Local/5@Long-Distance-00000000;2", "") in new stack
    -- Executing [5@Internal-Setup:2] Return("Local/5@Long-Distance-00000000;2", "") in new stack
    -- Executing [5@Local:3] Verbose("Local/5@Long-Distance-00000000;2", "1,"Local (after Internal-Setup): User 087xxxxxx4 dialed 5."") in new stack
 "Local (after Internal-Setup): User 087xxxxxx4 dialed 5."
    -- Executing [5@Local:4] Gosub("Local/5@Long-Distance-00000000;2", "Features,5,1") in new stack
    -- Channel PJSIP/justin-00000001 left 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
    -- Executing [5@Features:1] Return("Local/5@Long-Distance-00000000;2", "") in new stack
    -- Executing [5@Local:5] Verbose("Local/5@Long-Distance-00000000;2", "1,"Local (after Features): User 087xxxxxx4 dialed 5."") in new stack
    -- Channel PJSIP/justin-00000001 joined 'simple_bridge' basic-bridge <c9533522-de78-4103-a4f0-96b7bf2185d4>
 "Local (after Features): User 087xxxxxx4 dialed 5."
    -- Executing [5@Local:6] Gosub("Local/5@Long-Distance-00000000;2", "Dial-Users,5,1") in new stack
    -- Executing [5@Dial-Users:1] Verbose("Local/5@Long-Distance-00000000;2", "1,"Dial Users: User 087xxxxxx4 dialed 5."") in new stack
 "Dial Users: User 087xxxxxx4 dialed 5."
    -- Executing [5@Dial-Users:2] Gosub("Local/5@Long-Distance-00000000;2", "Dial-Users-Rules,5,1") in new stack
    -- Executing [5@Dial-Users-Rules:1] Goto("Local/5@Long-Distance-00000000;2", "Dial-Users-Rules,087xxxxxx5,1") in new stack
    -- Goto (Dial-Users-Rules,087xxxxxx5,1)
    -- Executing [087xxxxxx5@Dial-Users-Rules:1] Set("Local/5@Long-Distance-00000000;2", "SAC_DIALED_EXTEN=boardroom") in new stack
    -- Executing [087xxxxxx5@Dial-Users-Rules:2] Set("Local/5@Long-Distance-00000000;2", "SAC_VM_EXTEN=0120300141") in new stack
    -- Executing [087xxxxxx5@Dial-Users-Rules:3] Set("Local/5@Long-Distance-00000000;2", "DIAL1=PJSIP/boardroom") in new stack
    -- Executing [087xxxxxx5@Dial-Users-Rules:4] Goto("Local/5@Long-Distance-00000000;2", "sip3,1") in new stack
    -- Goto (Dial-Users-Rules,sip3,1)
    -- Executing [sip3@Dial-Users-Rules:1] Verbose("Local/5@Long-Distance-00000000;2", "1,"Dial-Users (SIP3): User 087xxxxxx4 dialed sip3: boardroom PJSIP/boardroom  ."") in new stack
 "Dial-Users (SIP3): User 087xxxxxx4 dialed sip3: boardroom PJSIP/boardroom  ."
    -- Executing [sip3@Dial-Users-Rules:2] Set("Local/5@Long-Distance-00000000;2", "__TRANSFER_CONTEXT=Long-Distance") in new stack
    -- Executing [sip3@Dial-Users-Rules:3] Verbose("Local/5@Long-Distance-00000000;2", "1,"PJSIP/boardroom - UNAVAILABLE"") in new stack
 "PJSIP/boardroom - UNAVAILABLE"
    -- Executing [sip3@Dial-Users-Rules:4] Set("Local/5@Long-Distance-00000000;2", "DIALALL=") in new stack
    -- Executing [sip3@Dial-Users-Rules:5] GotoIf("Local/5@Long-Distance-00000000;2", "1?dial2") in new stack
    -- Goto (Dial-Users-Rules,sip3,7)
    -- Executing [sip3@Dial-Users-Rules:7] Verbose("Local/5@Long-Distance-00000000;2", "1," - INVALID"") in new stack
 " - INVALID"
    -- Executing [sip3@Dial-Users-Rules:8] GotoIf("Local/5@Long-Distance-00000000;2", "1?dial3") in new stack
    -- Goto (Dial-Users-Rules,sip3,10)
    -- Executing [sip3@Dial-Users-Rules:10] Verbose("Local/5@Long-Distance-00000000;2", "1," - INVALID"") in new stack
 " - INVALID"
    -- Executing [sip3@Dial-Users-Rules:11] GotoIf("Local/5@Long-Distance-00000000;2", "1?dialx") in new stack
    -- Goto (Dial-Users-Rules,sip3,13)
    -- Executing [sip3@Dial-Users-Rules:13] Verbose("Local/5@Long-Distance-00000000;2", "1,"DIALALL: "") in new stack
 "DIALALL: "
    -- Executing [sip3@Dial-Users-Rules:14] GotoIf("Local/5@Long-Distance-00000000;2", "?:dialed-NOANSWER,1") in new stack
    -- Goto (Dial-Users-Rules,dialed-NOANSWER,1)
    -- Executing [dialed-NOANSWER@Dial-Users-Rules:1] NoOp("Local/5@Long-Distance-00000000;2", "") in new stack
    -- Executing [dialed-NOANSWER@Dial-Users-Rules:2] VoiceMail("Local/5@Long-Distance-00000000;2", "0120300141@default,u") in new stack
    -- <Local/5@Long-Distance-00000000;2> Playing 'vm-theperson.alaw' (language 'en')
    -- <Local/5@Long-Distance-00000000;2> Playing 'digits/0.alaw' (language 'en')
    -- <Local/5@Long-Distance-00000000;2> Playing 'digits/1.alaw' (language 'en')
    -- Channel PJSIP/justin-00000001 left 'simple_bridge' basic-bridge <c9533522-de78-4103-a4f0-96b7bf2185d4>
    -- Stopped music on hold on PJSIP/gavin-00000000
    -- Channel Local/5@Long-Distance-00000000;1 left 'simple_bridge' basic-bridge <c9533522-de78-4103-a4f0-96b7bf2185d4>
    -- Channel Local/5@Long-Distance-00000000;1 joined 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
    -- <Local/5@Long-Distance-00000000;1> Playing 'beep.alaw' (language 'en')
    -- <Local/5@Long-Distance-00000000;2> Playing 'digits/2.alaw' (language 'en')
    -- Channel PJSIP/gavin-00000000 left 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
  == Spawn extension (Dial-Users-Rules, sip3, 15) exited non-zero on 'PJSIP/gavin-00000000'
    -- Executing [h@Dial-Users-Rules:1] Verbose("PJSIP/gavin-00000000", "1, "CALL HUNG UP"") in new stack
  "CALL HUNG UP"
    -- Channel Local/5@Long-Distance-00000000;1 left 'simple_bridge' basic-bridge <1b7420f0-69b5-43f8-b43f-c61e10810974>
  == Spawn extension (Dial-Users-Rules, dialed-NOANSWER, 2) exited non-zero on 'Local/5@Long-Distance-00000000;2'
    -- Executing [h@Dial-Users-Rules:1] Verbose("Local/5@Long-Distance-00000000;2", "1, "CALL HUNG UP"") in new stack
  "CALL HUNG UP"
vmserver*CLI> 

Timeouts are correct. With ast_matchmore_extension() commented out, I can enter all 4 digits, and it starts dialling 3 seconds after I press the last digit.

With ast_matchmore_extension() in, it dials immediately after the first digit is entered.

The comment for matchmore says:

E_MATCHMORE =   0x00,   /* extension can match but only with more 'digits' */

So, I would expect this to return false only when the number can no longer match a valid extension? Which does not seem to be the case here.

And the features.conf so we have a full view of things? And Long-Distance context is the one to look at?

Pretty much defaults, with only the key sequences changed to match legacy systems.

;
; Sample Call Features (transfer, monitor/mixmonitor, etc) configuration
;

; Note: From Asterisk 12 - All parking lot configuration is now done in res_parking.conf

[general]
transferdigittimeout => 3       ; Number of seconds to wait between digits when transferring a call
pickupexten = *9               ; Configure the pickup extension. (default is *8)
pickupsound = beep             ; to indicate a successful pickup (default: no sound)
pickupfailsound = beeperr      ; to indicate that the pickup failed (default: no sound)
featuredigittimeout = 1000     ; Max time (ms) between digits for
atxferabort = #1               ; cancel the attended transfer
atxfercomplete = #2            ; complete the attended transfer, dropping out of the call
atxferthreeway = #3            ; complete the attended transfer, but stay in the call. This will turn the call into a multi-party bridge
atxferswap = #4                ; swap to the other party. Once an attended transfer has begun, this options may be used multiple times

; Note that the DTMF features listed below only work when two channels have answered and are bridged together.
; They can not be used while the remote party is ringing or in progress. If you require this feature you can use
; chan_local in combination with Answer to accomplish it.

[featuremap]
blindxfer => *1                ; Blind transfer  (default is #) -- Make sure to set the T and/or t option in the Dial() or Queue() app call!
atxfer => *2                   ; Attended transfer  -- Make sure to set the T and/or t option in the Dial() or Queue()  app call!
automixmon => *3               ; One Touch Record a.k.a. Touch MixMonitor -- Make sure to set the X and/or x option in the Dial() or Queue() app call!

[applicationmap]

The endpoint contexts are bitconew (for external trunk) and Long-Distance (calls placed from internal phones, and transfer context).

The ! in your _X! pattern overrides that. The longest match wildcard is ā€œ.ā€, although there are other differences (_X. requires two characters, but _X! only needs one).

1 Like

The question is really if this is intended behaviour, or a bug.

I do have overlapping extensions (5, 5226 and 52260 are all valid extensions). This works fine for SIP dialling, but it seems the DTMF behaviour may be to stop on the first valid extension match.

Unfortunately, this behaviour does not seem to be documented anywhere, and the code for ast_app_dtget() is does not mention this in the documentation/comments.

If the call to ast_matchmore_extension() is only intended to detect when a bad extension is dialled (number can not match, even when more digits are entered), then it may be a bug that it is exiting on a good, but ambiguous extension.

If ast_matchmore_extension() is intended to also exit on the first matching good extension, then the behaviour is by design, and I will just have to forever run modified code to remove this test. (Or find a good way to make this optional behaviour, and submit a patch.)

I would expect this to result in its stopping looking for more digits. The ! tells it to match as soon as it gets a minimal match. As far as I know, once a first level match is achieved, digit collection ends.

Although your numbering plan probably makes things more complex than this, the normal way of giving insiders both incoming and local calls is to use ā€œinclude =>ā€, i.e. a declarative process, not a procedural one. Even then, one has to be careful, as matches can be accepted before any or all the ā€œinclude =>ā€ directives have been processed.

OK. So it is basically by design that Asterisk does not support overlapping extensions when dialling by DTMF.

Would it be worth trying to work out a patch to enable this, or is my use case niche enough that it is better to just comment out that test each time I build?

As I said, I don’t think overlapping numbers is a good idea, so it not something I’ve investigated in detail, but I think, as long as you give it all the options in one go, and use . rather than ! it can do it. I think it is a combination of the procedural, rather than purely declarative, elements in your dialplan, and the use of ! that is causing your problems.

I’m old enough to have used, albeit not worked on, Strowger exchanges, and those have no capability to backtrack.

It’s only really when on hook dialling became the norm, that this sort of arrangement became common.

Note that . matches at least one digit, whereas ! matches zero or more.

                ...
                }
                collect[x++] = res;
                if (!ast_matchmore_extension(chan, context, collect, 1,
                        S_COR(ast_channel_caller(chan)->id.number.valid, ast_channel_caller(chan)->id.number.str, NULL))) {
                        break;
                }
        }

        if (res >= 0) {
                res = ast_exists_extension(chan, context, collect, 1,
                        S_COR(ast_channel_caller(chan)->id.number.valid, ast_channel_caller(chan)->id.number.str, NULL)) ? 1 : 0;
        }

I am not sure that will help. In order for ast_app_dtget() to succeed, the extension must match ast_exists_extension(), but by design, the ast_matchmore_extension() test will exit on anything that matches ast_exists_extension().
So, no matter how you define the extensions, ast_app_dtget() will always exit on the first complete match, or will fail.
ast_matchmore_extension() would need to be tweaked to fail on ambiguous partial matches before any changes to the dialplan would help.

This works, I know because I just did it:

[Long-Distance]
exten = _+27X!,1,Goto(Long-Distance-Rules,0${EXTEN:3},1)
exten = _+X!,1,Goto(Long-Distance-Rules,${EXTEN:1},1)
exten = _X,1,Goto(Long-Distance-Rules,${EXTEN},1)
exten = _X.,1,Goto(Long-Distance-Rules,${EXTEN},1)

I think you would have to replace the ! on the first two lines, for + format numbers to work properly.

@david551 Since this is DTMF based attended transfer I didn’t touch the first two, as they won’t be encountered for this.

Thanks. I will give this a try after office hours tomorrow. Still not entirely sure how this works, but I am happy if I am understanding the code wrong.

Just to check - I have 3 levels of overlapping codes - 1, 4 and 5 digits. So should I have rules for:

*_X
_*XXXX
_XXXX.

?

You are trying to match all possible numbers, allowing for some timeouts. I think _Xand _X.covers that. _.is not recommended, as it matches special extension codes, which is why you need the two patterns.

Where there is scope for more patterns is to avoid last digit timeouts, when enough digits have been entered to get past the overlap, but that is an optimisation, in the first level processing. I haven’t analyzed the whole code, so I don’t know if there are other places where there is a problem, but the current proposal should ensure that all numbers, starting with a digit, are subject to a timeout.

(I preferred the older forum handling of pre-formatted text mark up, where you could see the and edit the markup characters. I had to set it type some extra spaces, move the cursor back, turn on preformatted text, and type in the text, then move the cursor back into the typed ahead spaces, and do the same for the next bit. I’ve had similar issues with links.)

OK - that worked perfectly. I still quite understand how the logic in pbx.c works, but I am glad that the solution is so simple. Thanks for the help!

On a side note, I did notice in ast_app_dtget() that playtones() should probably be bypassed if you enter with a non-empty ā€˜collect’ or it confuses the user if they start dialling during the ā€˜transfer’ prompt, and then they re-enter the first digit.

Thanks!

Justin