chlog: freetdm: Added support for RESTART timeout, treat RESTART's without restart indicator IE as restart on full interface

This commit is contained in:
David Yat Sin 2011-07-15 17:56:48 -04:00
parent 33aa1cb763
commit 2c9bccbfc8
7 changed files with 133 additions and 114 deletions

View File

@ -154,12 +154,18 @@ typedef struct ftdm_sngisdn_prog_ind {
} ftdm_sngisdn_progind_t;
/* Only timers that can be cancelled are listed here */
#define SNGISDN_NUM_TIMERS 2
/* Increase NUM_TIMERS as number of ftdm_sngisdn_timer_t increases */
typedef enum {
SNGISDN_TIMER_FACILITY = 0,
SNGISDN_TIMER_ATT_TRANSFER,
} ftdm_sngisdn_timer_t;
#define SNGISDN_NUM_CHAN_TIMERS 2
/* Increase SNGISDN_NUM_CHAN_TIMERS as number of ftdm_sngisdn_chan_timer_t increases */
typedef enum {
SNGISDN_CHAN_TIMER_FACILITY,
SNGISDN_CHAN_TIMER_ATT_TRANSFER,
} ftdm_sngisdn_chan_timer_t;
#define SNGISDN_NUM_SPAN_TIMERS 1
/* Increase SNGISDN_NUM_SPAN_TIMERS as number of ftdm_sngisdn_spanan_timer_t increases */
typedef enum {
SNGISDN_SPAN_TIMER_RESTART,
} ftdm_sngisdn_span_timer_t;
typedef struct sngisdn_glare_data {
int16_t suId;
@ -210,7 +216,7 @@ typedef struct sngisdn_chan_data {
uint8_t globalFlg;
sngisdn_glare_data_t glare;
ftdm_timer_id_t timers[SNGISDN_NUM_TIMERS];
ftdm_timer_id_t timers[SNGISDN_NUM_CHAN_TIMERS];
sngisdn_transfer_data_t transfer_data;
/* variables saved here will be sent to the user application
@ -252,8 +258,10 @@ typedef struct sngisdn_span_data {
uint8_t raw_trace_q921; /* TODO: combine with trace_flags */
uint8_t timer_t3;
uint8_t restart_opt;
uint8_t restart_timeout;
uint8_t force_sending_complete;
char* local_numbers[SNGISDN_NUM_LOCAL_NUMBERS];
ftdm_timer_id_t timers[SNGISDN_NUM_SPAN_TIMERS];
ftdm_sched_t *sched;
ftdm_queue_t *event_queue;
} sngisdn_span_data_t;
@ -514,6 +522,7 @@ void sngisdn_delayed_connect(void* p_sngisdn_info);
void sngisdn_delayed_disconnect(void* p_sngisdn_info);
void sngisdn_facility_timeout(void* p_sngisdn_info);
void sngisdn_t3_timeout(void* p_sngisdn_info);
void sngisdn_restart_timeout(void* p_signal_data);
/* Stack management functions */
ftdm_status_t sngisdn_stack_cfg(ftdm_span_t *span);

View File

@ -359,6 +359,8 @@ ftdm_status_t ftmod_isdn_parse_cfg(ftdm_conf_parameter_t *ftdm_parameters, ftdm_
ftdm_set_bearer_layer1(val, (uint8_t*)&span->default_caller_data.bearer_layer1);
} else if (!strcasecmp(var, "channel-restart-on-link-up")) {
parse_yesno(var, val, &signal_data->restart_opt);
} else if (!strcasecmp(var, "channel-restart-timeout")) {
signal_data->restart_timeout = atoi(val);
} else if (!strcasecmp(var, "local-number")) {
if (add_local_number(val, span) != FTDM_SUCCESS) {
return FTDM_FAIL;

View File

@ -681,12 +681,7 @@ ftdm_status_t sngisdn_stack_cfg_q931_dlsap(ftdm_span_t *span)
cfg.t.cfg.s.inDLSAP.clrGlr = FALSE; /* in case of glare, do not clear local call */
cfg.t.cfg.s.inDLSAP.statEnqOpt = TRUE;
if (signal_data->switchtype == SNGISDN_SWITCH_EUROISDN ||
signal_data->switchtype == SNGISDN_SWITCH_INSNET) {
cfg.t.cfg.s.inDLSAP.rstOpt = FALSE;
} else {
cfg.t.cfg.s.inDLSAP.rstOpt = TRUE;
}
cfg.t.cfg.s.inDLSAP.rstOpt = TRUE;
} else {
cfg.t.cfg.s.inDLSAP.ackOpt = FALSE;
cfg.t.cfg.s.inDLSAP.intType = USER;

View File

@ -34,8 +34,7 @@
#include "ftmod_sangoma_isdn.h"
static ftdm_status_t sngisdn_cause_val_requires_disconnect(ftdm_channel_t *ftdmchan, CauseDgn *causeDgn);
static void sngisdn_process_restart_confirm(ftdm_channel_t *ftdmchan);
static ftdm_status_t sngisdn_force_down(ftdm_channel_t *ftdmchan);
static ftdm_status_t sngisdn_bring_down(ftdm_channel_t *ftdmchan);
/* Remote side transmit a SETUP */
void sngisdn_process_con_ind (sngisdn_event_data_t *sngisdn_event)
@ -185,7 +184,7 @@ void sngisdn_process_con_ind (sngisdn_event_data_t *sngisdn_event)
/* Launch timer in case we never get a FACILITY msg */
if (signal_data->facility_timeout) {
ftdm_sched_timer(signal_data->sched, "facility_timeout", signal_data->facility_timeout,
sngisdn_facility_timeout, (void*) sngisdn_info, &sngisdn_info->timers[SNGISDN_TIMER_FACILITY]);
sngisdn_facility_timeout, (void*) sngisdn_info, &sngisdn_info->timers[SNGISDN_CHAN_TIMER_FACILITY]);
}
break;
} else if (ret_val == 0) {
@ -836,7 +835,7 @@ void sngisdn_process_fac_ind (sngisdn_event_data_t *sngisdn_event)
}
if (signal_data->facility_timeout) {
/* Cancel facility timeout */
ftdm_sched_cancel_timer(signal_data->sched, sngisdn_info->timers[SNGISDN_TIMER_FACILITY]);
ftdm_sched_cancel_timer(signal_data->sched, sngisdn_info->timers[SNGISDN_CHAN_TIMER_FACILITY]);
}
}
@ -927,7 +926,7 @@ void sngisdn_process_sta_cfm (sngisdn_event_data_t *sngisdn_event)
switch(call_state) {
/* Sere ITU-T Q931 for definition of call states */
case 0: /* Remote switch thinks there are no calls on this channel */
if (sngisdn_force_down(ftdmchan) != FTDM_SUCCESS) {
if (sngisdn_bring_down(ftdmchan) != FTDM_SUCCESS) {
ftdm_log_chan(ftdmchan, FTDM_LOG_CRIT, "Don't know how to handle incompatible state. remote call state:%d our state:%s\n", call_state, ftdm_channel_state2str(ftdmchan->state));
}
break;
@ -1121,29 +1120,12 @@ void sngisdn_process_srv_cfm (sngisdn_event_data_t *sngisdn_event)
return;
}
static void sngisdn_process_restart_confirm(ftdm_channel_t *ftdmchan)
{
switch (ftdmchan->state) {
case FTDM_CHANNEL_STATE_RESET:
ftdm_set_state(ftdmchan, FTDM_CHANNEL_STATE_DOWN);
break;
case FTDM_CHANNEL_STATE_DOWN:
/* Do nothing */
break;
default:
ftdm_log_chan(ftdmchan, FTDM_LOG_CRIT, "Received RESTART CFM in an invalid state (%s)\n",
ftdm_channel_state2str(ftdmchan->state));
}
return;
}
static ftdm_status_t sngisdn_force_down(ftdm_channel_t *ftdmchan)
static ftdm_status_t sngisdn_bring_down(ftdm_channel_t *ftdmchan)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)ftdmchan->call_data;
ftdm_status_t status = FTDM_SUCCESS;
ftdm_log_chan(ftdmchan, FTDM_LOG_DEBUG, "Forcing channel to DOWN state (%s)\n", ftdm_channel_state2str(ftdmchan->state));
ftdm_log_chan(ftdmchan, FTDM_LOG_DEBUG, "Bringing channel to DOWN state (%s)\n", ftdm_channel_state2str(ftdmchan->state));
switch (ftdmchan->state) {
case FTDM_CHANNEL_STATE_DOWN:
/* Do nothing */
@ -1196,38 +1178,36 @@ void sngisdn_process_rst_cfm (sngisdn_event_data_t *sngisdn_event)
return;
}
if (rstEvnt->rstInd.eh.pres != PRSNT_NODEF && rstEvnt->rstInd.rstClass.pres != PRSNT_NODEF) {
ftdm_log(FTDM_LOG_DEBUG, "Received RESTART, but Restart Indicator IE not present\n");
return;
}
switch(rstEvnt->rstInd.rstClass.val) {
case IN_CL_INDCHAN: /* Indicated b-channel */
if (rstEvnt->chanId.eh.pres) {
if (rstEvnt->chanId.intType.val == IN_IT_BASIC) {
if (rstEvnt->chanId.infoChanSel.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.infoChanSel.val;
}
} else if (rstEvnt->chanId.intType.val == IN_IT_OTHER) {
if (rstEvnt->chanId.chanNmbSlotMap.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.chanNmbSlotMap.val[0];
if (rstEvnt->rstInd.eh.pres == PRSNT_NODEF && rstEvnt->rstInd.rstClass.pres == PRSNT_NODEF) {
switch(rstEvnt->rstInd.rstClass.val) {
case IN_CL_INDCHAN: /* Indicated b-channel */
if (rstEvnt->chanId.eh.pres) {
if (rstEvnt->chanId.intType.val == IN_IT_BASIC) {
if (rstEvnt->chanId.infoChanSel.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.infoChanSel.val;
}
} else if (rstEvnt->chanId.intType.val == IN_IT_OTHER) {
if (rstEvnt->chanId.chanNmbSlotMap.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.chanNmbSlotMap.val[0];
}
}
}
}
if (!chan_no) {
ftdm_log(FTDM_LOG_CRIT, "Failed to determine channel from RESTART\n");
if (!chan_no) {
ftdm_log(FTDM_LOG_CRIT, "Failed to determine channel from RESTART\n");
return;
}
break;
case IN_CL_SNGINT: /* Single interface */
case IN_CL_ALLINT: /* All interfaces */
/* In case restart class indicates all interfaces, we will duplicate
this event on each span associated to this d-channel in sngisdn_rcv_rst_cfm,
so treat it as a single interface anyway */
chan_no = 0;
break;
default:
ftdm_log(FTDM_LOG_CRIT, "Invalid restart indicator class:%d\n", rstEvnt->rstInd.rstClass.val);
return;
}
break;
case IN_CL_SNGINT: /* Single interface */
case IN_CL_ALLINT: /* All interfaces */
/* In case restart class indicates all interfaces, we will duplicate
this event on each span associated to this d-channel in sngisdn_rcv_rst_cfm,
so treat it as a single interface anyway */
break;
default:
ftdm_log(FTDM_LOG_CRIT, "Invalid restart indicator class:%d\n", rstEvnt->rstInd.rstClass.val);
return;
}
}
if (chan_no) { /* For a single channel */
@ -1235,7 +1215,7 @@ void sngisdn_process_rst_cfm (sngisdn_event_data_t *sngisdn_event)
ftdm_log(FTDM_LOG_CRIT, "Received RESTART on invalid channel:%d\n", chan_no);
} else {
ftdm_channel_t *ftdmchan = ftdm_span_get_channel(signal_data->ftdm_span, chan_no);
sngisdn_process_restart_confirm(ftdmchan);
sngisdn_bring_down(ftdmchan);
}
} else { /* for all channels */
ftdm_iterator_t *chaniter = NULL;
@ -1243,7 +1223,7 @@ void sngisdn_process_rst_cfm (sngisdn_event_data_t *sngisdn_event)
chaniter = ftdm_span_get_chan_iterator(signal_data->ftdm_span, NULL);
for (curr = chaniter; curr; curr = ftdm_iterator_next(curr)) {
sngisdn_process_restart_confirm((ftdm_channel_t*)ftdm_iterator_current(curr));
sngisdn_bring_down((ftdm_channel_t*)ftdm_iterator_current(curr));
}
ftdm_iterator_free(chaniter);
}
@ -1272,10 +1252,6 @@ void sngisdn_process_rst_ind (sngisdn_event_data_t *sngisdn_event)
/* TODO: readjust this when NFAS is implemented as signal_data will not always be the first
* span for that d-channel */
if (!rstEvnt->rstInd.eh.pres || !rstEvnt->rstInd.rstClass.pres) {
ftdm_log(FTDM_LOG_DEBUG, "Received RESTART IND, but Restart Indicator IE not present\n");
return;
}
signal_data = g_sngisdn_data.dchans[dChan].spans[1];
@ -1283,46 +1259,48 @@ void sngisdn_process_rst_ind (sngisdn_event_data_t *sngisdn_event)
ftdm_log(FTDM_LOG_CRIT, "Received RESTART IND on unconfigured span (suId:%d)\n", suId);
return;
}
if (signal_data->restart_timeout) {
ftdm_sched_cancel_timer(signal_data->sched, signal_data->timers[SNGISDN_SPAN_TIMER_RESTART]);
}
ftdm_log(FTDM_LOG_DEBUG, "Processing RESTART IND (suId:%u dChan:%d ces:%d %s)\n", suId, dChan, ces,
ftdm_log(FTDM_LOG_DEBUG, "Processing RESTART IND (suId:%u dChan:%d ces:%d %s(%d))\n", suId, dChan, ces,
(evntType == IN_LNK_DWN)?"LNK_DOWN":
(evntType == IN_LNK_UP)?"LNK_UP":
(evntType == IN_INDCHAN)?"b-channel":
(evntType == IN_LNK_DWN_DM_RLS)?"NFAS service procedures":
(evntType == IN_SWCHD_BU_DCHAN)?"NFAS switchover to backup":"Unknown");
(evntType == IN_SWCHD_BU_DCHAN)?"NFAS switchover to backup":"Unknown", evntType);
if (!rstEvnt->rstInd.eh.pres || !rstEvnt->rstInd.rstClass.pres) {
ftdm_log(FTDM_LOG_DEBUG, "Received RESTART IND, but Restart Indicator IE not present\n");
return;
}
switch(rstEvnt->rstInd.rstClass.val) {
case IN_CL_INDCHAN: /* Indicated b-channel */
if (rstEvnt->chanId.eh.pres) {
if (rstEvnt->chanId.intType.val == IN_IT_BASIC) {
if (rstEvnt->chanId.infoChanSel.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.infoChanSel.val;
}
} else if (rstEvnt->chanId.intType.val == IN_IT_OTHER) {
if (rstEvnt->chanId.chanNmbSlotMap.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.chanNmbSlotMap.val[0];
if (rstEvnt->rstInd.eh.pres == PRSNT_NODEF && rstEvnt->rstInd.rstClass.pres == PRSNT_NODEF) {
switch(rstEvnt->rstInd.rstClass.val) {
case IN_CL_INDCHAN: /* Indicated b-channel */
if (rstEvnt->chanId.eh.pres) {
if (rstEvnt->chanId.intType.val == IN_IT_BASIC) {
if (rstEvnt->chanId.infoChanSel.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.infoChanSel.val;
}
} else if (rstEvnt->chanId.intType.val == IN_IT_OTHER) {
if (rstEvnt->chanId.chanNmbSlotMap.pres == PRSNT_NODEF) {
chan_no = rstEvnt->chanId.chanNmbSlotMap.val[0];
}
}
}
}
if (!chan_no) {
ftdm_log(FTDM_LOG_CRIT, "Failed to determine channel from RESTART\n");
if (!chan_no) {
ftdm_log(FTDM_LOG_CRIT, "Failed to determine channel from RESTART\n");
return;
}
break;
case IN_CL_SNGINT: /* Single interface */
case IN_CL_ALLINT: /* All interfaces */
/* In case restart class indicates all interfaces, we will duplicate
this event on each span associated to this d-channel in sngisdn_rcv_rst_cfm,
so treat it as a single interface anyway */
chan_no = 0;
break;
default:
ftdm_log(FTDM_LOG_CRIT, "Invalid restart indicator class:%d\n", rstEvnt->rstInd.rstClass.val);
return;
}
break;
case IN_CL_SNGINT: /* Single interface */
case IN_CL_ALLINT: /* All interfaces */
/* In case restart class indicates all interfaces, we will duplicate
this event on each span associated to this d-channel in sngisdn_rcv_rst_cfm,
so treat it as a single interface anyway */
break;
default:
ftdm_log(FTDM_LOG_CRIT, "Invalid restart indicator class:%d\n", rstEvnt->rstInd.rstClass.val);
return;
}
}
if (chan_no) { /* For a single channel */
@ -1336,7 +1314,7 @@ void sngisdn_process_rst_ind (sngisdn_event_data_t *sngisdn_event)
for (curr = chaniter; curr; curr = ftdm_iterator_next(curr)) {
ftdm_channel_t *ftdmchan = (ftdm_channel_t*)ftdm_iterator_current(curr);
if (ftdmchan->physical_chan_id == chan_no) {
sngisdn_force_down(ftdmchan);
sngisdn_bring_down(ftdmchan);
}
}
ftdm_iterator_free(chaniter);
@ -1347,7 +1325,7 @@ void sngisdn_process_rst_ind (sngisdn_event_data_t *sngisdn_event)
chaniter = ftdm_span_get_chan_iterator(signal_data->ftdm_span, NULL);
for (curr = chaniter; curr; curr = ftdm_iterator_next(curr)) {
sngisdn_force_down((ftdm_channel_t*)ftdm_iterator_current(curr));
sngisdn_bring_down((ftdm_channel_t*)ftdm_iterator_current(curr));
}
ftdm_iterator_free(chaniter);
}

View File

@ -903,6 +903,17 @@ int16_t sngisdn_rcv_l1_data_req(uint16_t spId, sng_l1_frame_t *l1_frame)
if ((flags & FTDM_WRITE)) {
#if 0
int i;
char string [2000];
unsigned string_len = 0;
for (i = 0; i < length; i++) {
string_len += sprintf(&string[string_len], "0x%02x ", l1_frame->data[i]);
}
ftdm_log_chan(signal_data->dchan, FTDM_LOG_CRIT, "\nTX [%s]\n", string);
#endif
status = signal_data->dchan->fio->write(signal_data->dchan, l1_frame->data, (ftdm_size_t*)&length);
if (status != FTDM_SUCCESS) {
ftdm_log_chan_msg(signal_data->dchan, FTDM_LOG_CRIT, "Failed to transmit frame\n");

View File

@ -976,7 +976,7 @@ ftdm_status_t set_restart_ind_ie(ftdm_channel_t *ftdmchan, RstInd *rstInd)
return FTDM_SUCCESS;
}
void sngisdn_t3_timeout(void* p_sngisdn_info)
void sngisdn_t3_timeout(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;
@ -998,7 +998,31 @@ void sngisdn_t3_timeout(void* p_sngisdn_info)
ftdm_mutex_unlock(ftdmchan->mutex);
}
void sngisdn_delayed_setup(void* p_sngisdn_info)
void sngisdn_restart_timeout(void *p_signal_data)
{
sngisdn_span_data_t *signal_data = (sngisdn_span_data_t *)p_signal_data;
ftdm_span_t *span = signal_data->ftdm_span;
ftdm_log(FTDM_LOG_DEBUG, "s%d:Did not receive a RESTART from remote switch in %d ms - restarting\n", span->name, signal_data->restart_timeout);
ftdm_iterator_t *chaniter = NULL;
ftdm_iterator_t *curr = NULL;
chaniter = ftdm_span_get_chan_iterator(span, NULL);
for (curr = chaniter; curr; curr = ftdm_iterator_next(curr)) {
ftdm_channel_t *ftdmchan = (ftdm_channel_t*)ftdm_iterator_current(curr);
if (FTDM_IS_VOICE_CHANNEL(ftdmchan)) {
ftdm_mutex_lock(ftdmchan->mutex);
if (ftdmchan->state == FTDM_CHANNEL_STATE_DOWN) {
ftdm_set_state(ftdmchan, FTDM_CHANNEL_STATE_RESET);
}
ftdm_mutex_unlock(ftdmchan->mutex);
}
}
return;
}
void sngisdn_delayed_setup(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;
@ -1009,7 +1033,7 @@ void sngisdn_delayed_setup(void* p_sngisdn_info)
return;
}
void sngisdn_delayed_release(void* p_sngisdn_info)
void sngisdn_delayed_release(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;
@ -1032,7 +1056,7 @@ void sngisdn_delayed_release(void* p_sngisdn_info)
return;
}
void sngisdn_delayed_connect(void* p_sngisdn_info)
void sngisdn_delayed_connect(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;
@ -1047,7 +1071,7 @@ void sngisdn_delayed_connect(void* p_sngisdn_info)
return;
}
void sngisdn_delayed_disconnect(void* p_sngisdn_info)
void sngisdn_delayed_disconnect(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;
@ -1069,7 +1093,7 @@ void sngisdn_delayed_disconnect(void* p_sngisdn_info)
return;
}
void sngisdn_facility_timeout(void* p_sngisdn_info)
void sngisdn_facility_timeout(void *p_sngisdn_info)
{
sngisdn_chan_data_t *sngisdn_info = (sngisdn_chan_data_t*)p_sngisdn_info;
ftdm_channel_t *ftdmchan = sngisdn_info->ftdmchan;

View File

@ -164,7 +164,7 @@ static ftdm_status_t att_courtesy_vru(ftdm_channel_t *ftdmchan, sngisdn_transfer
sngisdn_send_signal(sngisdn_info, FTDM_SIGEVENT_UP);
}
if (signal_data->transfer_timeout) {
ftdm_sched_timer(((sngisdn_span_data_t*)ftdmchan->span->signal_data)->sched, "courtesy_transfer_timeout", signal_data->transfer_timeout, att_courtesy_transfer_timeout, (void*) sngisdn_info, &sngisdn_info->timers[SNGISDN_TIMER_ATT_TRANSFER]);
ftdm_sched_timer(((sngisdn_span_data_t*)ftdmchan->span->signal_data)->sched, "courtesy_transfer_timeout", signal_data->transfer_timeout, att_courtesy_transfer_timeout, (void*) sngisdn_info, &sngisdn_info->timers[SNGISDN_CHAN_TIMER_ATT_TRANSFER]);
}
status = FTDM_SUCCESS;
@ -260,7 +260,7 @@ ftdm_status_t sngisdn_att_transfer_process_dtmf(ftdm_channel_t *ftdmchan, const
sngisdn_info->transfer_data.response = FTDM_TRANSFER_RESPONSE_INVALID;
}
if (signal_data->transfer_timeout) {
ftdm_sched_cancel_timer(signal_data->sched, sngisdn_info->timers[SNGISDN_TIMER_ATT_TRANSFER]);
ftdm_sched_cancel_timer(signal_data->sched, sngisdn_info->timers[SNGISDN_CHAN_TIMER_ATT_TRANSFER]);
}
if (sngisdn_info->transfer_data.response == FTDM_TRANSFER_RESPONSE_OK &&