Skip to content

Commit 6198b9a

Browse files
committed
res_smdr_whozz: Fix FXO-in-use detection.
Update the code to detect if associated Asterisk device was in use, and thus whether to skip a CDR since Asterisk will be doing it. Previously, the original logic of using the device state failed on incoming calls. Even if not answered through Asterisk, the CDR would get skipped because the device was initially "In Use" and when the call ended it was not. This is the case because when an FXO port is ringing, its device state is "In Use", not "Ringing". Thus, we need to check the channel state of the channel using the device, which allows distinguishing between on-hook ("Ring") and off-hook ("Up").
1 parent e8aabef commit 6198b9a

File tree

1 file changed

+117
-4
lines changed

1 file changed

+117
-4
lines changed

res/res_smdr_whozz.c

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<synopsis>Asterisk device</synopsis>
6969
<description>
7070
<para>Asterisk device corresponding to this SMDR line, if applicable.</para>
71+
<para>If provided, this must be a <literal>chan_dahdi</literal> FXO (FXS-signalled) device connected to the same phone line.</para>
7172
<para>If specified, SMDR entries will be ignored if this channel is in use during the logged call.
7273
This is useful if certain calls are made through an FXO port with CDR already directly logged by Asterisk directly,
7374
but other calls are made directly on the line and not through the FXO port. Setting this option appropriately
@@ -132,7 +133,8 @@ struct whozz_line {
132133
struct ast_channel *chan; /*!< Dummy channel for CDR */
133134
const char *device; /*!< Asterisk device */
134135
enum line_state state; /*!< Current line state */
135-
enum ast_device_state startstate; /*!< Starting device state of associated device, if applicable */
136+
enum ast_device_state startstate; /*!< Starting device state of associated FXO device, if applicable */
137+
enum ast_device_state answerstate; /*!< Device state of associated FXO device, if applicable, at time of off-hook on incoming call */
136138
struct varshead varshead; /*!< Variables to set on channel for CDR */
137139
AST_LIST_ENTRY(whozz_line) entry; /*!< Next channel */
138140
char data[];
@@ -354,6 +356,94 @@ static void mark_answered(struct ast_channel *chan)
354356
ast_channel_unlock(chan);
355357
}
356358

359+
static int fxo_device_state(const char *device)
360+
{
361+
/* Non-ISDN DAHDI channels (e.g. analog) are always "unknown" when not in use...
362+
* not sure I agree with this, but this is the way it is currently, so account for that.
363+
*
364+
* Furthermore, the device state is misleading and CANNOT BE USED for determining the line state.
365+
* - When the connected phone line is idle, the state will be "Unknown".
366+
* - When the FXO port is in use, the state will be "In Use" (good).
367+
* - When there is an incoming call ringing to the FXO port, its device state will be... "In Use".
368+
*
369+
* To understand what's going on, recall the Asterisk fallback for computing device state simply
370+
* finds a channel associated with the device and converts its channel state to a mapped device state.
371+
*
372+
* AST_STATE_RING -> AST_DEVICE_INUSE
373+
* AST_STATE_RINGING -> AST_DEVICE_RINGING
374+
*
375+
* STATE_RING essentially corresponds to audible ring being present on the channel, while
376+
* STATE_RINGING corresponds to the device actually ringing, e.g. actual physical 90V power ring.
377+
*
378+
* However, RINGING only makes sense when Asterisk is ringing a device (whether it's DAHDI or not).
379+
* For example, when ringing a phone, its DAHDI channel will have channel state (and device state) "Ringing".
380+
*
381+
* However, on incoming calls to an FXO port, Asterisk isn't ringing it... the opposite, in fact!
382+
* The network is indicating to us that there is an incoming call. And in all other scenarios,
383+
* the incoming channel side that is ringing a phone has state "Ring". FXO ports are a special edge
384+
* case where there is something physically ringing (perhaps phones on the connected line), but it's
385+
* not really "Ringing" in the semantic sense that Asterisk uses it.
386+
*
387+
* TL;DR To distinguish between FXO port being rung and actually active and in use, do not use
388+
* ${DEVICE_STATE(DAHDI/1)}
389+
* In both cases, it will return "INUSE".
390+
* Instead, do:
391+
* ${IMPORT(DAHDI/1-1,CHANNEL(state))}
392+
* This is because chan_dahdi doesn't support call waiting on FXO ports (currently),
393+
* so there will only ever be one channel max on an FXO port, and so we know what it will be called.
394+
* Therefore, if we know the device is in use, then we can use this to check if Asterisk is off-hook on the FXO port.
395+
* If it returns "Ring", it's just ringing and the FXO port is idle.
396+
* If it returns "Up", then we're actually off-hook on the FXO port.
397+
*
398+
* The code below does the internal equivalent of ${IMPORT(DAHDI/1-1,CHANNEL(state))}
399+
*/
400+
401+
/* Use the device state to get started, but for the reasons described at length above, we can't use that alone.
402+
* We need to narrow it down further to distinguish between ringing and actually in use. */
403+
enum ast_device_state devstate = ast_device_state(device);
404+
if (devstate == AST_DEVICE_INUSE) {
405+
struct ast_channel *chan2;
406+
char channel_name[64];
407+
/* The DAHDI channel naming scheme is predictable, and FXO ports are only going to have 1 channel, currently,
408+
* so that simplifies this to a straightforward translation. */
409+
snprintf(channel_name, sizeof(channel_name), "%s-1", device);
410+
if ((chan2 = ast_channel_get_by_name(channel_name))) {
411+
enum ast_channel_state state;
412+
ast_channel_lock(chan2);
413+
state = ast_channel_state(chan2);
414+
ast_channel_unlock(chan2);
415+
chan2 = ast_channel_unref(chan2);
416+
417+
/* Based on the state, do a conversion. */
418+
ast_debug(3, "Channel state of %s is %s\n", channel_name, ast_state2str(state));
419+
switch (state) {
420+
case AST_STATE_RING:
421+
/* Normally, this would map to AST_DEVICE_INUSE.
422+
* For our purposes, we return AST_DEVICE_RINGING,
423+
* to reflect the FXO port ringing. Semantically,
424+
* this breaks with Asterisk's idea of what device state
425+
* refers to, but we're really just repurposing the enum
426+
* for something specific here. */
427+
devstate = AST_DEVICE_RINGING;
428+
break;
429+
default:
430+
/* Leave it alone */
431+
break;
432+
}
433+
} else {
434+
/* Not really any way to determine the truth... */
435+
ast_log(LOG_ERROR, "Channel %s does not exist\n", channel_name);
436+
}
437+
} else if (devstate == AST_DEVICE_UNKNOWN) {
438+
/* chan_dahdi doesn't set specific states for non-ISDN.
439+
* If it's unknown, then what that really indicates is not in use. */
440+
devstate = AST_DEVICE_NOT_INUSE;
441+
}
442+
return devstate;
443+
}
444+
445+
/* NOTE: Do not use the ast_device_state function directly below this point! Use fxo_device_state instead! */
446+
357447
static int handle_hook(struct whozz_line *w, int outbound, int end, int duration, const char *numberstr, const char *cnam)
358448
{
359449
if (end) {
@@ -362,14 +452,29 @@ static int handle_hook(struct whozz_line *w, int outbound, int end, int duration
362452

363453
/* End call and finalize */
364454
if (!ast_strlen_zero(w->device)) {
365-
enum ast_device_state endstate = ast_device_state(w->device);
366-
if (w->startstate == AST_DEVICE_INUSE && endstate == AST_DEVICE_NOT_INUSE) {
455+
int ast_device_used;
456+
enum ast_device_state endstate = fxo_device_state(w->device);
457+
if (outbound) {
458+
ast_device_used = w->startstate == AST_DEVICE_INUSE && endstate == AST_DEVICE_NOT_INUSE;
459+
} else {
460+
/* The inbound case is a little bit different because there's an extra step.
461+
* Initially, the state should always be AST_DEVICE_RINGING here,
462+
* because you can't answer a phone call through Asterisk before the FXO port even starts ringing!
463+
* But RINGING to start with and NOT_INUSE at the end doesn't tell us whether we answered through Asterisk or not.
464+
* The critical thing is detecting DEVICE_INUSE at some point while the line is off-hook,
465+
* and conveniently we check this right if/when an off-hook occurs. */
466+
ast_device_used = w->answerstate == AST_DEVICE_INUSE && endstate == AST_DEVICE_NOT_INUSE;
467+
}
468+
if (ast_device_used) {
367469
/* Avoid a duplicate CDR record, since the call was made through Asterisk. */
368470
ast_verb(6, "Call was made through associated device, ignoring this call for SMDR purposes\n");
369471
__cleanup_stale_cdr(w->chan);
370472
w->chan = NULL;
371473
return 0;
474+
} else {
475+
ast_debug(2, "FXO state of %s was %s and is now %s\n", w->device, ast_devstate_str(w->startstate), ast_devstate_str(endstate));
372476
}
477+
w->startstate = w->answerstate = AST_DEVICE_UNKNOWN; /* Reset */
373478
}
374479

375480
/* Now, add any variables */
@@ -404,7 +509,7 @@ static int handle_hook(struct whozz_line *w, int outbound, int end, int duration
404509
* If it's in use, then we know that the call in question
405510
* is being made through Asterisk, and thus we'll probably
406511
* end up ignoring this call for CDR purposes. */
407-
w->startstate = ast_device_state(w->device);
512+
w->startstate = fxo_device_state(w->device);
408513
}
409514

410515
/* Unfortunately, we cannot use a dummy channel for CDR.
@@ -573,6 +678,9 @@ static int __process_serial_read(struct whozz_line *w, int lineno, const char *a
573678
/* Answer it on the channel */
574679
if (w->chan) {
575680
mark_answered(w->chan);
681+
if (!ast_strlen_zero(w->device)) {
682+
w->answerstate = fxo_device_state(w->device);
683+
}
576684
} else {
577685
ast_log(LOG_WARNING, "No call in progress, ignoring call answer\n");
578686
}
@@ -727,6 +835,7 @@ static int serial_loop(struct pollfd *pfd)
727835
for (;;) {
728836
char buf[128];
729837
char *pos;
838+
730839
bufres = poll(pfd, 1, -1);
731840
if (bufres <= 0) {
732841
if (unloading) {
@@ -904,6 +1013,10 @@ static int load_config(void)
9041013
}
9051014
} else if (!strcasecmp(var->name, "device")) {
9061015
device = var->value;
1016+
if (strncasecmp(device, "DAHDI/", 6)) {
1017+
ast_log(LOG_WARNING, "Setting 'device' must be a DAHDI device\n");
1018+
device = NULL;
1019+
}
9071020
} else if (!strcasecmp(var->name, "setvar")) {
9081021
continue; /* Ignore on this pass */
9091022
} else {

0 commit comments

Comments
 (0)