1882 lines
44 KiB
C
1882 lines
44 KiB
C
/*
|
|
* chan_dongle
|
|
*
|
|
* Copyright (C) 2011-2015
|
|
* bg <bg_one@mail.ru>
|
|
* http://www.e1550.mobi
|
|
*
|
|
* chan_dongle is based on chan_datacard by
|
|
*
|
|
* Artem Makhutov <artem@makhutov.org>
|
|
* http://www.makhutov.org
|
|
*
|
|
* Dmitry Vagin <dmitry2004@yandex.ru>
|
|
*
|
|
* chan_datacard is based on chan_mobile by Digium
|
|
* (Mark Spencer <markster@digium.com>)
|
|
*
|
|
* This program is free software, distributed under the terms of
|
|
* the GNU General Public License Version 2. See the LICENSE file
|
|
* at the top of the source tree.
|
|
*/
|
|
|
|
/*! \file
|
|
*
|
|
* \brief UMTS Voice Dongle channel driver
|
|
*
|
|
* \author Artem Makhutov <artem@makhutov.org>
|
|
* \author Dave Bowerman <david.bowerman@gmail.com>
|
|
* \author Dmitry Vagin <dmitry2004@yandex.ru>
|
|
* \author bg <bg_one@mail.ru>
|
|
* \author Max von Buelow <max@m9x.de>
|
|
*
|
|
* \ingroup channel_drivers
|
|
*/
|
|
#include "ast_config.h"
|
|
|
|
#if ASTERISK_VERSION_NUM < 140000 /* 14- */
|
|
ASTERISK_FILE_VERSION(__FILE__, "$Rev: " PACKAGE_REVISION " $")
|
|
#endif /* 14- */
|
|
|
|
#include <asterisk/stringfields.h> /* AST_DECLARE_STRING_FIELDS for asterisk/manager.h */
|
|
#include <asterisk/manager.h>
|
|
#include <asterisk/dsp.h>
|
|
#include <asterisk/callerid.h>
|
|
#include <asterisk/module.h> /* AST_MODULE_LOAD_DECLINE ... */
|
|
#include <asterisk/timing.h> /* ast_timer_open() ast_timer_fd() */
|
|
|
|
#include <sys/stat.h> /* S_IRUSR | S_IRGRP | S_IROTH */
|
|
#include <termios.h> /* struct termios tcgetattr() tcsetattr() */
|
|
#include <pthread.h> /* pthread_t pthread_kill() pthread_join() */
|
|
#include <fcntl.h> /* O_RDWR O_NOCTTY */
|
|
#include <signal.h> /* SIGURG */
|
|
|
|
#include "ast_compat.h" /* asterisk compatibility fixes */
|
|
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
#include <asterisk/stasis_channels.h>
|
|
#include <asterisk/format_cache.h>
|
|
#endif /* ^13+ */
|
|
|
|
#include "chan_dongle.h"
|
|
#include "at_response.h" /* at_res_t */
|
|
#include "at_queue.h" /* struct at_queue_task_cmd at_queue_head_cmd() */
|
|
#include "at_command.h" /* at_cmd2str() */
|
|
#include "mutils.h" /* ITEMS_OF() */
|
|
#include "at_read.h"
|
|
#include "cli.h"
|
|
#include "app.h"
|
|
#include "manager.h"
|
|
#include "channel.h" /* channel_queue_hangup() */
|
|
#include "dc_config.h" /* dc_uconfig_fill() dc_gconfig_fill() dc_sconfig_fill() */
|
|
#include "pdiscovery.h" /* pdiscovery_lookup() pdiscovery_init() pdiscovery_fini() */
|
|
#include "smsdb.h"
|
|
#include "error.h"
|
|
|
|
EXPORT_DEF const char * const dev_state_strs[4] = { "stop", "restart", "remove", "start" };
|
|
EXPORT_DEF public_state_t * gpublic;
|
|
#if ASTERISK_VERSION_NUM >= 100000 && ASTERISK_VERSION_NUM < 130000 /* 10-13 */
|
|
EXPORT_DEF struct ast_format chan_dongle_format;
|
|
EXPORT_DEF struct ast_format_cap * chan_dongle_format_cap;
|
|
#endif /* ^10-13 */
|
|
|
|
static int public_state_init(struct public_state * state);
|
|
|
|
|
|
/*!
|
|
* Get status of the dongle. It might happen that the device disappears
|
|
* (e.g. due to a USB unplug).
|
|
*
|
|
* \return 0 if device seems ok, non-0 if it seems not available
|
|
*/
|
|
|
|
static int port_status (int fd)
|
|
{
|
|
struct termios t;
|
|
|
|
if (fd < 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return tcgetattr (fd, &t);
|
|
}
|
|
|
|
#/* return length of lockname */
|
|
static int lock_build(const char * devname, char * buf, unsigned length)
|
|
{
|
|
const char * basename;
|
|
char resolved_path[PATH_MAX];
|
|
|
|
/* follow symlinks */
|
|
if(realpath(devname, resolved_path) != NULL)
|
|
devname = resolved_path;
|
|
|
|
basename = strrchr(devname, '/');
|
|
if(basename)
|
|
basename++;
|
|
else
|
|
basename = devname;
|
|
|
|
/* NOTE: use system system wide lock directory */
|
|
#if defined(__FreeBSD__)
|
|
return snprintf(buf, length, "/var/spool/lock/LCK..%s", basename);
|
|
#else
|
|
return snprintf(buf, length, "/var/lock/LCK..%s", basename);
|
|
#endif
|
|
}
|
|
|
|
#/* return 0 on error */
|
|
static int lock_create(const char * lockfile)
|
|
{
|
|
int fd;
|
|
int len = 0;
|
|
char pidb[21];
|
|
|
|
fd = open(lockfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IRGRP | S_IROTH);
|
|
if(fd >= 0)
|
|
{
|
|
/* NOTE: bg: i assume next open reuse same fd - not thread-safe */
|
|
len = snprintf(pidb, sizeof(pidb), "%d %d", getpid(), fd);
|
|
len = write(fd, pidb, len);
|
|
close(fd);
|
|
} else {
|
|
ast_log(LOG_ERROR, "open('%s') failed: %s\n", lockfile, strerror(errno));
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
#/* return pid of owner, 0 if free */
|
|
EXPORT_DEF int lock_try(const char * devname, char ** lockname)
|
|
{
|
|
int fd;
|
|
int len;
|
|
int pid = 0;
|
|
int assigned;
|
|
int fd2;
|
|
char name[1024];
|
|
char buffer[65];
|
|
|
|
lock_build(devname, name, sizeof(name));
|
|
|
|
/* FIXME: rise conditions: some time between lock check and got lock */
|
|
fd = open(name, O_RDONLY);
|
|
if(fd >= 0)
|
|
{
|
|
len = read(fd, buffer, sizeof(buffer) - 1);
|
|
if(len > 0)
|
|
{
|
|
buffer[len] = 0;
|
|
assigned = sscanf(buffer, "%d %d", &len, &fd2);
|
|
if(assigned > 0 && kill(len, 0) == 0)
|
|
{
|
|
if(len == getpid() && assigned > 1)
|
|
{
|
|
if(port_status(fd2) == 0)
|
|
pid = len;
|
|
}
|
|
else
|
|
pid = len;
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
if(pid == 0)
|
|
{
|
|
unlink(name);
|
|
lock_create(name);
|
|
*lockname = ast_strdup(name);
|
|
}
|
|
return pid;
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF void closetty(int fd, char ** lockfname)
|
|
{
|
|
close(fd);
|
|
|
|
/* remove lock */
|
|
unlink(*lockfname);
|
|
ast_free(*lockfname);
|
|
*lockfname = NULL;
|
|
}
|
|
|
|
EXPORT_DEF int opentty (const char* dev, char ** lockfile)
|
|
{
|
|
int flags;
|
|
int pid;
|
|
int fd;
|
|
struct termios term_attr;
|
|
char buf[40];
|
|
|
|
|
|
pid = lock_try(dev, lockfile);
|
|
if(pid != 0)
|
|
{
|
|
ast_log (LOG_WARNING, "%s already used by process %d\n", dev, pid);
|
|
return -1;
|
|
}
|
|
|
|
fd = open(dev, O_RDWR | O_NOCTTY);
|
|
if (fd < 0)
|
|
{
|
|
flags = errno;
|
|
closetty(fd, lockfile);
|
|
snprintf(buf, sizeof(buf), "Open Failed\r\nErrorCode: %d", flags);
|
|
manager_event_message_raw("DonglePortFail", dev, buf);
|
|
ast_log (LOG_WARNING, "unable to open %s: %s\n", dev, strerror(flags));
|
|
return -1;
|
|
}
|
|
/* Put the terminal into exclusive mode. All other open(2)s by
|
|
* non-root will fail with EBUSY. */
|
|
if (ioctl(fd, TIOCEXCL) != 0) {
|
|
ast_log(LOG_WARNING, "ioctl(TIOCEXCL) failed for %s: %s\n", dev, strerror(errno));
|
|
}
|
|
|
|
flags = fcntl(fd, F_GETFD);
|
|
if (flags == -1 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1)
|
|
{
|
|
flags = errno;
|
|
closetty(fd, lockfile);
|
|
ast_log (LOG_WARNING, "fcntl(F_GETFD/F_SETFD) failed for %s: %s\n", dev, strerror(flags));
|
|
return -1;
|
|
}
|
|
|
|
if (tcgetattr (fd, &term_attr) != 0)
|
|
{
|
|
flags = errno;
|
|
closetty(fd, lockfile);
|
|
ast_log (LOG_WARNING, "tcgetattr() failed for %s: %s\n", dev, strerror(flags));
|
|
return -1;
|
|
}
|
|
|
|
term_attr.c_cflag = B115200 | CS8 | CREAD | CRTSCTS;
|
|
term_attr.c_iflag = 0;
|
|
term_attr.c_oflag = 0;
|
|
term_attr.c_lflag = 0;
|
|
term_attr.c_cc[VMIN] = 1;
|
|
term_attr.c_cc[VTIME] = 0;
|
|
|
|
if (tcsetattr (fd, TCSAFLUSH, &term_attr) != 0)
|
|
{
|
|
ast_log (LOG_WARNING, "tcsetattr(TCSAFLUSH) failed for %s: %s\n", dev, strerror(errno));
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
#/* phone monitor thread pvt cleanup */
|
|
static void disconnect_dongle (struct pvt* pvt)
|
|
{
|
|
struct cpvt * cpvt, * next;
|
|
|
|
if (!PVT_NO_CHANS(pvt))
|
|
{
|
|
ast_debug (1, "[%s] Dongle disconnecting, hanging up channels\n", PVT_ID(pvt));
|
|
|
|
for(cpvt = pvt->chans.first; cpvt; cpvt = next)
|
|
{
|
|
next = cpvt->entry.next;
|
|
at_hangup_immediality(cpvt);
|
|
CPVT_RESET_FLAGS(cpvt, CALL_FLAG_NEED_HANGUP);
|
|
change_channel_state(cpvt, CALL_STATE_RELEASED, 0);
|
|
}
|
|
}
|
|
at_queue_flush(pvt);
|
|
pvt->last_dialed_cpvt = NULL;
|
|
|
|
closetty (pvt->audio_fd, &pvt->alock);
|
|
closetty (pvt->data_fd, &pvt->dlock);
|
|
|
|
pvt->data_fd = -1;
|
|
pvt->audio_fd = -1;
|
|
|
|
if(pvt->dsp)
|
|
ast_dsp_digitreset(pvt->dsp);
|
|
pvt_on_remove_last_channel(pvt);
|
|
|
|
/* pvt->a_write_rb */
|
|
|
|
pvt->dtmf_digit = 0;
|
|
pvt->rings = 0;
|
|
|
|
// else
|
|
{
|
|
/* unaffected in case of restart */
|
|
pvt->gsm_reg_status = -1;
|
|
pvt->rssi = 0;
|
|
pvt->linkmode = 0;
|
|
pvt->linksubmode = 0;
|
|
ast_copy_string (pvt->provider_name, "NONE", sizeof (pvt->provider_name));
|
|
pvt->manufacturer[0] = '\0';
|
|
pvt->model[0] = '\0';
|
|
pvt->firmware[0] = '\0';
|
|
pvt->imei[0] = '\0';
|
|
pvt->imsi[0] = '\0';
|
|
pvt->has_subscriber_number = 0;
|
|
ast_copy_string (pvt->subscriber_number, "Unknown", sizeof (pvt->subscriber_number));
|
|
pvt->location_area_code[0] = '\0';
|
|
pvt->cell_id[0] = '\0';
|
|
pvt->sms_scenter[0] = '\0';
|
|
|
|
pvt->gsm_registered = 0;
|
|
pvt->has_sms = 0;
|
|
pvt->has_voice = 0;
|
|
pvt->has_call_waiting = 0;
|
|
}
|
|
|
|
pvt->connected = 0;
|
|
pvt->initialized = 0;
|
|
pvt->has_call_waiting = 0;
|
|
|
|
/* FIXME: LOST real device state */
|
|
pvt->dialing = 0;
|
|
pvt->ring = 0;
|
|
pvt->cwaiting = 0;
|
|
pvt->outgoing_sms = 0;
|
|
pvt->incoming_sms_index = -1U;
|
|
pvt->volume_sync_step = VOLUME_SYNC_BEGIN;
|
|
|
|
pvt->current_state = DEV_STATE_STOPPED;
|
|
|
|
/* clear statictics */
|
|
memset(&pvt->stat, 0, sizeof(pvt->stat));
|
|
|
|
ast_copy_string (PVT_STATE(pvt, data_tty), CONF_UNIQ(pvt, data_tty), sizeof (PVT_STATE(pvt, data_tty)));
|
|
ast_copy_string (PVT_STATE(pvt, audio_tty), CONF_UNIQ(pvt, audio_tty), sizeof (PVT_STATE(pvt, audio_tty)));
|
|
|
|
ast_verb (3, "[%s] Dongle has disconnected\n", PVT_ID(pvt));
|
|
|
|
manager_event_device_status(PVT_ID(pvt), "Disconnect");
|
|
}
|
|
|
|
#define SMS_INBOX_BIT(index) ((sms_inbox_item_type)(1) << (index % SMS_INBOX_ITEM_BITS))
|
|
#define SMS_INBOX_INDEX(index) (index / SMS_INBOX_ITEM_BITS)
|
|
|
|
static int is_sms_inbox_index_valid(const struct pvt* pvt, int index)
|
|
{
|
|
if (index < 0 || index >= (int) SMS_INDEX_MAX) {
|
|
ast_log(LOG_WARNING, "[%s] SMS index [%d] out of range\n", PVT_ID(pvt), index);
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
EXPORT_DEF int sms_inbox_set(struct pvt* pvt, int index)
|
|
{
|
|
if (!is_sms_inbox_index_valid(pvt, index)) {
|
|
return 0;
|
|
}
|
|
|
|
pvt->incoming_sms_inbox[SMS_INBOX_INDEX(index)] |= SMS_INBOX_BIT(index);
|
|
return 1;
|
|
}
|
|
|
|
EXPORT_DEF int sms_inbox_clear(struct pvt* pvt, int index)
|
|
{
|
|
if (!is_sms_inbox_index_valid(pvt, index)) {
|
|
return 0;
|
|
}
|
|
|
|
pvt->incoming_sms_inbox[SMS_INBOX_INDEX(index)] &= ~SMS_INBOX_BIT(index);
|
|
return 1;
|
|
}
|
|
|
|
EXPORT_DEF int is_sms_inbox_set(const struct pvt* pvt, int index)
|
|
{
|
|
if (!is_sms_inbox_index_valid(pvt, index)) {
|
|
return 0;
|
|
}
|
|
|
|
return pvt->incoming_sms_inbox[SMS_INBOX_INDEX(index)] & SMS_INBOX_BIT(index);
|
|
}
|
|
|
|
#undef SMS_INBOX_INDEX
|
|
#undef SMS_INBOX_BIT
|
|
|
|
/* anybody wrote some to device before me, and not read results, clean pending results here */
|
|
#/* */
|
|
EXPORT_DEF void clean_read_data(const char * devname, int fd)
|
|
{
|
|
char buf[2*1024];
|
|
struct ringbuffer rb;
|
|
int iovcnt;
|
|
int t;
|
|
|
|
rb_init (&rb, buf, sizeof (buf));
|
|
for (t = 0; at_wait(fd, &t); t = 0)
|
|
{
|
|
iovcnt = at_read (fd, devname, &rb);
|
|
ast_debug (4, "[%s] drop %u bytes of pending data before initialization\n", devname, (unsigned)rb_used(&rb));
|
|
/* drop read */
|
|
rb_init (&rb, buf, sizeof (buf));
|
|
if (iovcnt == 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void handle_expired_reports(struct pvt *pvt)
|
|
{
|
|
char dst[SMSDB_DST_MAX_LEN];
|
|
char payload[SMSDB_PAYLOAD_MAX_LEN];
|
|
ssize_t payload_len = smsdb_outgoing_purge_one(dst, payload);
|
|
if (payload_len >= 0) {
|
|
ast_verb (3, "[%s] TTL payload: %.*s\n", PVT_ID(pvt), (int) payload_len, payload);
|
|
channel_var_t vars[] =
|
|
{
|
|
{ "SMS_REPORT_PAYLOAD", payload },
|
|
{ "SMS_REPORT_TS", "" },
|
|
{ "SMS_REPORT_DT", "" },
|
|
{ "SMS_REPORT_SUCCESS", "0" },
|
|
{ "SMS_REPORT_TYPE", "t" },
|
|
{ "SMS_REPORT", "" },
|
|
{ NULL, NULL },
|
|
};
|
|
start_local_channel(pvt, "report", dst, vars);
|
|
manager_event_report(PVT_ID(pvt), payload, payload_len, "", "", 0, 2, "");
|
|
}
|
|
}
|
|
|
|
|
|
/*!
|
|
* \brief Check if the module is unloading.
|
|
* \retval 0 not unloading
|
|
* \retval 1 unloading
|
|
*/
|
|
|
|
static void* do_monitor_phone (void* data)
|
|
{
|
|
struct pvt* pvt = (struct pvt*) data;
|
|
at_res_t at_res;
|
|
const struct at_queue_cmd * ecmd;
|
|
int t;
|
|
char buf[2*1024];
|
|
struct ringbuffer rb;
|
|
struct iovec iov[2];
|
|
int iovcnt;
|
|
char dev[sizeof(PVT_ID(pvt))];
|
|
int fd;
|
|
int read_result = 0;
|
|
|
|
pvt->timeout = DATA_READ_TIMEOUT;
|
|
rb_init (&rb, buf, sizeof (buf));
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
/* 4 reduce locking time make copy of this readonly fields */
|
|
fd = pvt->data_fd;
|
|
ast_copy_string(dev, PVT_ID(pvt), sizeof(dev));
|
|
|
|
clean_read_data(dev, fd);
|
|
|
|
/* schedule dongle initilization */
|
|
if (at_enqueue_initialization(&pvt->sys_chan, CMD_AT))
|
|
{
|
|
ast_log (LOG_ERROR, "[%s] Error adding initialization commands to queue\n", dev);
|
|
goto e_cleanup;
|
|
}
|
|
|
|
/* Poll first SMS, if any */
|
|
if (at_poll_sms(pvt) == 0)
|
|
{
|
|
ast_debug (1, "[%s] Polling first SMS message\n", PVT_ID(pvt));
|
|
}
|
|
|
|
ast_mutex_unlock (&pvt->lock);
|
|
|
|
|
|
while (1)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
handle_expired_reports(pvt);
|
|
|
|
if (port_status (pvt->data_fd) || port_status (pvt->audio_fd))
|
|
{
|
|
ast_log (LOG_ERROR, "[%s] Lost connection to Dongle\n", dev);
|
|
goto e_cleanup;
|
|
}
|
|
|
|
if(pvt->terminate_monitor)
|
|
{
|
|
ast_log (LOG_NOTICE, "[%s] stopping by %s request\n", dev, dev_state2str(pvt->desired_state));
|
|
goto e_restart;
|
|
}
|
|
|
|
t = at_queue_timeout(pvt);
|
|
if(t < 0)
|
|
t = pvt->timeout;
|
|
|
|
ast_mutex_unlock (&pvt->lock);
|
|
|
|
if (!at_wait (fd, &t))
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
ecmd = at_queue_head_cmd (pvt);
|
|
if(ecmd)
|
|
{
|
|
ast_log (LOG_ERROR, "[%s] timedout while waiting '%s' in response to '%s'\n", dev, at_res2str (ecmd->res), at_cmd2str (ecmd->cmd));
|
|
goto e_cleanup;
|
|
}
|
|
at_enqueue_ping(&pvt->sys_chan);
|
|
ast_mutex_unlock (&pvt->lock);
|
|
continue;
|
|
}
|
|
|
|
/* FIXME: access to device not locked */
|
|
iovcnt = at_read (fd, dev, &rb);
|
|
if (iovcnt < 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
PVT_STAT(pvt, d_read_bytes) += iovcnt;
|
|
ast_mutex_unlock (&pvt->lock);
|
|
|
|
while ((iovcnt = at_read_result_iov (dev, &read_result, &rb, iov)) > 0)
|
|
{
|
|
at_res = at_read_result_classification (&rb, iov[0].iov_len + iov[1].iov_len);
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
PVT_STAT(pvt, at_responses) ++;
|
|
if (at_response (pvt, iov, iovcnt, at_res) || at_queue_run(pvt))
|
|
{
|
|
goto e_cleanup;
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
e_cleanup:
|
|
if (!pvt->initialized)
|
|
{
|
|
// TODO: send monitor event
|
|
ast_verb (3, "[%s] Error initializing Dongle\n", dev);
|
|
}
|
|
/* it real, unsolicited disconnect */
|
|
pvt->terminate_monitor = 0;
|
|
|
|
e_restart:
|
|
disconnect_dongle (pvt);
|
|
// pvt->monitor_running = 0;
|
|
ast_mutex_unlock (&pvt->lock);
|
|
|
|
/* TODO: wakeup discovery thread after some delay */
|
|
return NULL;
|
|
}
|
|
|
|
static inline int start_monitor (struct pvt * pvt)
|
|
{
|
|
if (ast_pthread_create_background (&pvt->monitor_thread, NULL, do_monitor_phone, pvt) < 0)
|
|
{
|
|
pvt->monitor_thread = AST_PTHREADT_NULL;
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
#/* */
|
|
static void pvt_stop(struct pvt * pvt)
|
|
{
|
|
pthread_t id;
|
|
|
|
if(pvt->monitor_thread != AST_PTHREADT_NULL)
|
|
{
|
|
pvt->terminate_monitor = 1;
|
|
pthread_kill (pvt->monitor_thread, SIGURG);
|
|
id = pvt->monitor_thread;
|
|
|
|
ast_mutex_unlock (&pvt->lock);
|
|
pthread_join (id, NULL);
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
pvt->terminate_monitor = 0;
|
|
pvt->monitor_thread = AST_PTHREADT_NULL;
|
|
}
|
|
// pvt->current_state = DEV_STATE_STOPPED;
|
|
}
|
|
|
|
#/* called with pvt lock hold */
|
|
static int pvt_discovery(struct pvt * pvt)
|
|
{
|
|
char devname[DEVNAMELEN];
|
|
char imei[IMEI_SIZE+1];
|
|
char imsi[IMSI_SIZE+1];
|
|
|
|
int resolved;
|
|
if(CONF_UNIQ(pvt, data_tty)[0] == 0 && CONF_UNIQ(pvt, audio_tty)[0] == 0) {
|
|
char * data_tty;
|
|
char * audio_tty;
|
|
|
|
ast_copy_string(devname, PVT_ID(pvt), sizeof(devname));
|
|
ast_copy_string(imei, CONF_UNIQ(pvt, imei), sizeof(imei));
|
|
ast_copy_string(imsi, CONF_UNIQ(pvt, imsi), sizeof(imsi));
|
|
|
|
ast_debug(3, "[%s] Trying ports discovery for%s%s%s%s\n",
|
|
PVT_ID(pvt),
|
|
imei[0] == 0 ? "" : " IMEI ",
|
|
imei,
|
|
imsi[0] == 0 ? "" : " IMSI ",
|
|
imsi
|
|
);
|
|
ast_mutex_unlock (&pvt->lock);
|
|
//sleep(10); // test
|
|
resolved = pdiscovery_lookup(devname, imei, imsi, &data_tty, &audio_tty);
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
if(resolved) {
|
|
ast_copy_string (PVT_STATE(pvt, data_tty), data_tty, sizeof (PVT_STATE(pvt, data_tty)));
|
|
ast_copy_string (PVT_STATE(pvt, audio_tty), audio_tty, sizeof (PVT_STATE(pvt, audio_tty)));
|
|
|
|
ast_free(audio_tty);
|
|
ast_free(data_tty);
|
|
ast_verb (3, "[%s]%s%s%s%s found on data_tty=%s audio_tty=%s\n",
|
|
PVT_ID(pvt),
|
|
imei[0] == 0 ? "" : " IMEI ",
|
|
imei,
|
|
imsi[0] == 0 ? "" : " IMSI ",
|
|
imsi,
|
|
PVT_STATE(pvt, data_tty),
|
|
PVT_STATE(pvt, audio_tty)
|
|
);
|
|
} else {
|
|
ast_debug(3, "[%s] Not found ports for%s%s%s%s\n",
|
|
PVT_ID(pvt),
|
|
imei[0] == 0 ? "" : " IMEI ",
|
|
imei,
|
|
imsi[0] == 0 ? "" : " IMSI ",
|
|
imsi
|
|
);
|
|
}
|
|
} else {
|
|
ast_copy_string (PVT_STATE(pvt, data_tty), CONF_UNIQ(pvt, data_tty), sizeof (PVT_STATE(pvt, data_tty)));
|
|
ast_copy_string (PVT_STATE(pvt, audio_tty), CONF_UNIQ(pvt, audio_tty), sizeof (PVT_STATE(pvt, audio_tty)));
|
|
resolved = 1;
|
|
}
|
|
return ! resolved;
|
|
}
|
|
|
|
#/* */
|
|
static void pvt_start(struct pvt * pvt)
|
|
{
|
|
long flags;
|
|
|
|
/* prevent start_monitor() multiple times and on turned off devices */
|
|
if (pvt->connected || pvt->desired_state != DEV_STATE_STARTED) {
|
|
// || (pvt->monitor_thread != AST_PTHREADT_NULL &&
|
|
// (pthread_kill(pvt->monitor_thread, 0) == 0 || errno != ESRCH))
|
|
return;
|
|
}
|
|
|
|
pvt_stop(pvt);
|
|
|
|
if (pvt_discovery(pvt)) {
|
|
return;
|
|
}
|
|
|
|
ast_verb(3, "[%s] Trying to connect on %s...\n", PVT_ID(pvt), PVT_STATE(pvt, data_tty));
|
|
|
|
pvt->data_fd = opentty(PVT_STATE(pvt, data_tty), &pvt->dlock);
|
|
if (pvt->data_fd < 0) {
|
|
return;
|
|
}
|
|
|
|
// TODO: delay until device activate voice call or at pvt_on_create_1st_channel()
|
|
pvt->audio_fd = opentty(PVT_STATE(pvt, audio_tty), &pvt->alock);
|
|
if (pvt->audio_fd < 0) {
|
|
goto cleanup_datafd;
|
|
}
|
|
|
|
if (!start_monitor(pvt)) {
|
|
goto cleanup_audiofd;
|
|
}
|
|
|
|
/* Set data_fd and audio_fd to non-blocking. This appears to fix
|
|
* incidental deadlocks occurring with Asterisk 12+ or with
|
|
* jitterbuffer enabled. Apparently Asterisk can call the
|
|
* (audio) read function for sockets that don't have data to
|
|
* read(). */
|
|
flags = fcntl(pvt->data_fd, F_GETFL);
|
|
fcntl(pvt->data_fd, F_SETFL, flags | O_NONBLOCK);
|
|
flags = fcntl(pvt->audio_fd, F_GETFL);
|
|
fcntl(pvt->audio_fd, F_SETFL, flags | O_NONBLOCK);
|
|
|
|
pvt->connected = 1;
|
|
pvt->current_state = DEV_STATE_STARTED;
|
|
manager_event_device_status(PVT_ID(pvt), "Connect");
|
|
ast_verb(3, "[%s] Dongle has connected, initializing...\n", PVT_ID(pvt));
|
|
return;
|
|
|
|
cleanup_audiofd:
|
|
closetty(pvt->audio_fd, &pvt->alock);
|
|
cleanup_datafd:
|
|
closetty(pvt->data_fd, &pvt->dlock);
|
|
}
|
|
|
|
#/* */
|
|
static void pvt_free(struct pvt * pvt)
|
|
{
|
|
at_queue_flush(pvt);
|
|
if(pvt->dsp)
|
|
ast_dsp_free(pvt->dsp);
|
|
|
|
ast_mutex_unlock(&pvt->lock);
|
|
|
|
ast_free(pvt);
|
|
}
|
|
|
|
#/* */
|
|
static void pvt_destroy(struct pvt * pvt)
|
|
{
|
|
ast_mutex_lock(&pvt->lock);
|
|
pvt_stop(pvt);
|
|
pvt_free(pvt);
|
|
|
|
}
|
|
|
|
static void * do_discovery(void * arg)
|
|
{
|
|
struct public_state * state = (struct public_state *) arg;
|
|
struct pvt * pvt;
|
|
|
|
while(state->unloading_flag == 0)
|
|
{
|
|
/* read lock for avoid deadlock when IMEI/IMSI discovery */
|
|
AST_RWLIST_RDLOCK(&state->devices);
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
pvt->must_remove = 0;
|
|
|
|
if(pvt->restart_time == RESTATE_TIME_NOW && pvt->desired_state != pvt->current_state)
|
|
{
|
|
switch(pvt->desired_state)
|
|
{
|
|
case DEV_STATE_RESTARTED:
|
|
pvt_stop(pvt);
|
|
pvt->desired_state = DEV_STATE_STARTED;
|
|
/* fall through */
|
|
case DEV_STATE_STARTED:
|
|
pvt_start(pvt);
|
|
break;
|
|
case DEV_STATE_REMOVED:
|
|
pvt_stop(pvt);
|
|
pvt->must_remove = 1;
|
|
break;
|
|
case DEV_STATE_STOPPED:
|
|
pvt_stop(pvt);
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
AST_RWLIST_UNLOCK (&state->devices);
|
|
|
|
/* actual device removal here for avoid long (discovery) time write lock on device list in loop above */
|
|
AST_RWLIST_WRLOCK(&state->devices);
|
|
AST_RWLIST_TRAVERSE_SAFE_BEGIN(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock(&pvt->lock);
|
|
if(pvt->must_remove)
|
|
{
|
|
AST_RWLIST_REMOVE_CURRENT(entry);
|
|
pvt_free(pvt);
|
|
} else
|
|
ast_mutex_unlock(&pvt->lock);
|
|
}
|
|
AST_RWLIST_TRAVERSE_SAFE_END;
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
|
|
/* Go to sleep (only if we are not unloading) */
|
|
if (state->unloading_flag == 0)
|
|
{
|
|
sleep(SCONF_GLOBAL(state, discovery_interval));
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
#/* */
|
|
static int discovery_restart(public_state_t * state)
|
|
{
|
|
if(state->discovery_thread == AST_PTHREADT_STOP)
|
|
return 0;
|
|
|
|
ast_mutex_lock(&state->discovery_lock);
|
|
if (state->discovery_thread == pthread_self()) {
|
|
ast_mutex_unlock(&state->discovery_lock);
|
|
ast_log(LOG_WARNING, "Cannot kill myself\n");
|
|
return -1;
|
|
}
|
|
if (state->discovery_thread != AST_PTHREADT_NULL) {
|
|
/* Wake up the thread */
|
|
pthread_kill(state->discovery_thread, SIGURG);
|
|
} else {
|
|
/* Start a new monitor */
|
|
if (ast_pthread_create_background(&state->discovery_thread, NULL, do_discovery, state) < 0) {
|
|
ast_mutex_unlock(&state->discovery_lock);
|
|
ast_log(LOG_ERROR, "Unable to start discovery thread\n");
|
|
return -1;
|
|
}
|
|
}
|
|
ast_mutex_unlock(&state->discovery_lock);
|
|
return 0;
|
|
}
|
|
|
|
#/* */
|
|
static void discovery_stop(public_state_t * state)
|
|
{
|
|
/* signal for discovery unloading */
|
|
state->unloading_flag = 1;
|
|
|
|
ast_mutex_lock(&state->discovery_lock);
|
|
if (state->discovery_thread && (state->discovery_thread != AST_PTHREADT_STOP) && (state->discovery_thread != AST_PTHREADT_NULL)) {
|
|
// pthread_cancel(state->discovery_thread);
|
|
pthread_kill(state->discovery_thread, SIGURG);
|
|
pthread_join(state->discovery_thread, NULL);
|
|
}
|
|
|
|
state->discovery_thread = AST_PTHREADT_STOP;
|
|
ast_mutex_unlock(&state->discovery_lock);
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF void pvt_on_create_1st_channel(struct pvt* pvt)
|
|
{
|
|
mixb_init (&pvt->a_write_mixb, pvt->a_write_buf, sizeof (pvt->a_write_buf));
|
|
// rb_init (&pvt->a_write_rb, pvt->a_write_buf, sizeof (pvt->a_write_buf));
|
|
|
|
if(!pvt->a_timer)
|
|
pvt->a_timer = ast_timer_open ();
|
|
|
|
/* FIXME: do on each channel switch */
|
|
if(pvt->dsp)
|
|
ast_dsp_digitreset (pvt->dsp);
|
|
pvt->dtmf_digit = 0;
|
|
pvt->dtmf_begin_time.tv_sec = 0;
|
|
pvt->dtmf_begin_time.tv_usec = 0;
|
|
pvt->dtmf_end_time.tv_sec = 0;
|
|
pvt->dtmf_end_time.tv_usec = 0;
|
|
|
|
manager_event_device_status(PVT_ID(pvt), "Used");
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF void pvt_on_remove_last_channel(struct pvt* pvt)
|
|
{
|
|
if (pvt->a_timer)
|
|
{
|
|
ast_timer_close(pvt->a_timer);
|
|
pvt->a_timer = NULL;
|
|
}
|
|
manager_event_device_status(PVT_ID(pvt), "Free");
|
|
}
|
|
|
|
#define SET_BIT(dw_array,bitno) do { (dw_array)[(bitno) >> 5] |= 1 << ((bitno) & 31) ; } while(0)
|
|
#define TEST_BIT(dw_array,bitno) ((dw_array)[(bitno) >> 5] & 1 << ((bitno) & 31))
|
|
#/* */
|
|
EXPORT_DEF int pvt_get_pseudo_call_idx(const struct pvt * pvt)
|
|
{
|
|
struct cpvt * cpvt;
|
|
int * bits;
|
|
int dwords = ((MAX_CALL_IDX + sizeof(*bits) - 1) / sizeof(*bits));
|
|
|
|
bits = alloca(dwords * sizeof(*bits));
|
|
memset(bits, 0, dwords * sizeof(*bits));
|
|
|
|
AST_LIST_TRAVERSE(&pvt->chans, cpvt, entry) {
|
|
SET_BIT(bits, cpvt->call_idx);
|
|
}
|
|
|
|
for(dwords = 1; dwords <= MAX_CALL_IDX; dwords++)
|
|
{
|
|
if(!TEST_BIT(bits, dwords))
|
|
return dwords;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#undef TEST_BIT
|
|
#undef SET_BIT
|
|
|
|
#/* */
|
|
static int is_dial_possible2(const struct pvt * pvt, int opts, const struct cpvt * ignore_cpvt)
|
|
{
|
|
struct cpvt * cpvt;
|
|
int hold = 0;
|
|
int active = 0;
|
|
// FIXME: allow HOLD states for CONFERENCE
|
|
int use_call_waiting = opts & CALL_FLAG_HOLD_OTHER;
|
|
|
|
if(pvt->ring || pvt->cwaiting || pvt->dialing)
|
|
return 0;
|
|
|
|
AST_LIST_TRAVERSE(&pvt->chans, cpvt, entry) {
|
|
switch(cpvt->state)
|
|
{
|
|
case CALL_STATE_INIT:
|
|
if(cpvt != ignore_cpvt)
|
|
return 0;
|
|
break;
|
|
|
|
case CALL_STATE_DIALING:
|
|
case CALL_STATE_ALERTING:
|
|
case CALL_STATE_INCOMING:
|
|
case CALL_STATE_WAITING:
|
|
return 0;
|
|
|
|
case CALL_STATE_ACTIVE:
|
|
if(hold || !use_call_waiting)
|
|
return 0;
|
|
active++;
|
|
break;
|
|
|
|
case CALL_STATE_ONHOLD:
|
|
if(active || !use_call_waiting)
|
|
return 0;
|
|
hold++;
|
|
break;
|
|
|
|
case CALL_STATE_RELEASED:
|
|
;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF int is_dial_possible(const struct pvt * pvt, int opts)
|
|
{
|
|
return is_dial_possible2(pvt, opts, NULL);
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DECL int pvt_enabled(const struct pvt * pvt)
|
|
{
|
|
return pvt->current_state == DEV_STATE_STARTED && (pvt->desired_state == pvt->current_state || pvt->restart_time == RESTATE_TIME_CONVENIENT);
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF int ready4voice_call(const struct pvt* pvt, const struct cpvt * current_cpvt, int opts)
|
|
{
|
|
if(!pvt->connected
|
|
|| !pvt->initialized
|
|
|| !pvt->has_voice
|
|
|| !pvt->gsm_registered
|
|
|| !pvt_enabled(pvt)) {
|
|
return 0;
|
|
}
|
|
|
|
return is_dial_possible2(pvt, opts, current_cpvt);
|
|
}
|
|
|
|
|
|
#/* */
|
|
static int can_dial(struct pvt* pvt, int opts, const struct ast_channel * requestor)
|
|
{
|
|
/* not allow hold requester channel :) */
|
|
/* FIXME: requestor may be just proxy/masquerade for real channel */
|
|
// use ast_bridged_channel(chan) ?
|
|
// use requestor->tech->get_base_channel() ?
|
|
|
|
if((opts & CALL_FLAG_HOLD_OTHER) == CALL_FLAG_HOLD_OTHER && channels_loop(pvt, requestor))
|
|
return 0;
|
|
return ready4voice_call(pvt, NULL, opts);
|
|
}
|
|
|
|
#/* return locked pvt or NULL */
|
|
EXPORT_DEF struct pvt * find_device_ex(struct public_state * state, const char * name)
|
|
{
|
|
struct pvt * pvt;
|
|
|
|
AST_RWLIST_RDLOCK(&state->devices);
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (!strcmp (PVT_ID(pvt), name))
|
|
{
|
|
break;
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
|
|
return pvt;
|
|
}
|
|
|
|
#/* return locked pvt or NULL */
|
|
EXPORT_DEF struct pvt * find_device_ext (const char * name)
|
|
{
|
|
struct pvt * pvt = find_device(name);
|
|
|
|
if (pvt) {
|
|
if (!pvt_enabled(pvt)) {
|
|
ast_mutex_unlock (&pvt->lock);
|
|
chan_dongle_err = E_DEVICE_DISABLED;
|
|
pvt = NULL;
|
|
}
|
|
} else {
|
|
chan_dongle_err = E_DEVICE_NOT_FOUND;
|
|
}
|
|
return pvt;
|
|
}
|
|
|
|
#/* like find_device but for resource spec; return locked! pvt or NULL */
|
|
EXPORT_DEF struct pvt * find_device_by_resource_ex(struct public_state * state, const char * resource, int opts, const struct ast_channel * requestor, int * exists)
|
|
{
|
|
int group;
|
|
size_t i;
|
|
size_t j;
|
|
size_t c;
|
|
size_t last_used;
|
|
struct pvt * pvt;
|
|
struct pvt * found = NULL;
|
|
struct pvt * round_robin[MAXDONGLEDEVICES];
|
|
|
|
*exists = 0;
|
|
/* Find requested device and make sure it's connected and initialized. */
|
|
AST_RWLIST_RDLOCK(&state->devices);
|
|
|
|
if (((resource[0] == 'g') || (resource[0] == 'G')) && ((resource[1] >= '0') && (resource[1] <= '9')))
|
|
{
|
|
errno = 0;
|
|
group = (int) strtol (&resource[1], (char**) NULL, 10);
|
|
if (errno != EINVAL)
|
|
{
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
|
|
if (CONF_SHARED(pvt, group) == group)
|
|
{
|
|
*exists = 1;
|
|
if(can_dial(pvt, opts, requestor))
|
|
{
|
|
found = pvt;
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
}
|
|
else if (((resource[0] == 'r') || (resource[0] == 'R')) && ((resource[1] >= '0') && (resource[1] <= '9')))
|
|
{
|
|
errno = 0;
|
|
group = (int) strtol (&resource[1], (char**) NULL, 10);
|
|
if (errno != EINVAL)
|
|
{
|
|
/* Generate a list of all available devices */
|
|
j = ITEMS_OF (round_robin);
|
|
c = 0; last_used = 0;
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (CONF_SHARED(pvt, group) == group)
|
|
{
|
|
round_robin[c] = pvt;
|
|
if (pvt->group_last_used == 1)
|
|
{
|
|
pvt->group_last_used = 0;
|
|
last_used = c;
|
|
}
|
|
|
|
c++;
|
|
|
|
if (c == j)
|
|
{
|
|
ast_mutex_unlock (&pvt->lock);
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
|
|
/* Search for a available device starting at the last used device */
|
|
for (i = 0, j = last_used + 1; i < c; i++, j++)
|
|
{
|
|
if (j == c)
|
|
{
|
|
j = 0;
|
|
}
|
|
|
|
pvt = round_robin[j];
|
|
*exists = 1;
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (can_dial(pvt, opts, requestor))
|
|
{
|
|
pvt->group_last_used = 1;
|
|
found = pvt;
|
|
break;
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
}
|
|
else if (((resource[0] == 'p') || (resource[0] == 'P')) && resource[1] == ':')
|
|
{
|
|
/* Generate a list of all available devices */
|
|
j = ITEMS_OF(round_robin);
|
|
c = 0; last_used = 0;
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (!strcmp (pvt->provider_name, &resource[2]))
|
|
{
|
|
round_robin[c] = pvt;
|
|
if (pvt->prov_last_used == 1)
|
|
{
|
|
pvt->prov_last_used = 0;
|
|
last_used = c;
|
|
}
|
|
|
|
c++;
|
|
|
|
if (c == j)
|
|
{
|
|
ast_mutex_unlock (&pvt->lock);
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
|
|
/* Search for a available device starting at the last used device */
|
|
for (i = 0, j = last_used + 1; i < c; i++, j++)
|
|
{
|
|
if (j == c)
|
|
{
|
|
j = 0;
|
|
}
|
|
|
|
pvt = round_robin[j];
|
|
*exists = 1;
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (can_dial(pvt, opts, requestor))
|
|
{
|
|
pvt->prov_last_used = 1;
|
|
found = pvt;
|
|
break;
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
else if (((resource[0] == 's') || (resource[0] == 'S')) && resource[1] == ':')
|
|
{
|
|
/* Generate a list of all available devices */
|
|
j = ITEMS_OF(round_robin);
|
|
c = 0; last_used = 0;
|
|
i = strlen (&resource[2]);
|
|
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (!strncmp (pvt->imsi, &resource[2], i))
|
|
{
|
|
round_robin[c] = pvt;
|
|
if (pvt->sim_last_used == 1)
|
|
{
|
|
pvt->sim_last_used = 0;
|
|
last_used = c;
|
|
}
|
|
|
|
c++;
|
|
|
|
if (c == j)
|
|
{
|
|
ast_mutex_unlock (&pvt->lock);
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
|
|
/* Search for a available device starting at the last used device */
|
|
for (i = 0, j = last_used + 1; i < c; i++, j++)
|
|
{
|
|
if (j == c)
|
|
{
|
|
j = 0;
|
|
}
|
|
|
|
pvt = round_robin[j];
|
|
*exists = 1;
|
|
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (can_dial(pvt, opts, requestor))
|
|
{
|
|
pvt->sim_last_used = 1;
|
|
found = pvt;
|
|
break;
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
else if (((resource[0] == 'i') || (resource[0] == 'I')) && resource[1] == ':')
|
|
{
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (!strcmp(pvt->imei, &resource[2]))
|
|
{
|
|
*exists = 1;
|
|
if(can_dial(pvt, opts, requestor))
|
|
{
|
|
found = pvt;
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock (&pvt->lock);
|
|
if (!strcmp (PVT_ID(pvt), resource))
|
|
{
|
|
*exists = 1;
|
|
if(can_dial(pvt, opts, requestor))
|
|
{
|
|
found = pvt;
|
|
break;
|
|
}
|
|
}
|
|
ast_mutex_unlock (&pvt->lock);
|
|
}
|
|
}
|
|
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
return found;
|
|
}
|
|
|
|
#/* */
|
|
static const char * pvt_state_base(const struct pvt * pvt)
|
|
{
|
|
const char * state = NULL;
|
|
// length is "AAAAAAAAAA"
|
|
if(pvt->current_state == DEV_STATE_STOPPED && pvt->desired_state == DEV_STATE_STOPPED)
|
|
state = "Stopped";
|
|
else if(!pvt->connected)
|
|
state = "Not connected";
|
|
else if(!pvt->initialized)
|
|
state = "Not initialized";
|
|
else if(!pvt->gsm_registered)
|
|
state = "GSM not registered";
|
|
return state;
|
|
}
|
|
|
|
|
|
#/* */
|
|
EXPORT_DEF const char* pvt_str_state(const struct pvt* pvt)
|
|
{
|
|
const char * state = pvt_state_base(pvt);
|
|
if(!state) {
|
|
if(pvt->ring || PVT_STATE(pvt, chan_count[CALL_STATE_INCOMING]))
|
|
state = "Ring";
|
|
else if(pvt->cwaiting || PVT_STATE(pvt, chan_count[CALL_STATE_WAITING]))
|
|
state = "Waiting";
|
|
else if(pvt->dialing ||
|
|
(PVT_STATE(pvt, chan_count[CALL_STATE_INIT])
|
|
+
|
|
PVT_STATE(pvt, chan_count[CALL_STATE_DIALING])
|
|
+
|
|
PVT_STATE(pvt, chan_count[CALL_STATE_ALERTING])) > 0)
|
|
state = "Dialing";
|
|
|
|
else if(PVT_STATE(pvt, chan_count[CALL_STATE_ACTIVE]) > 0)
|
|
// state = "Active";
|
|
state = pvt_call_dir(pvt);
|
|
else if(PVT_STATE(pvt, chan_count[CALL_STATE_ONHOLD]) > 0)
|
|
state = "Held";
|
|
else if(pvt->outgoing_sms || pvt->incoming_sms_index != -1U)
|
|
state = "SMS";
|
|
else
|
|
state = "Free";
|
|
}
|
|
return state;
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF struct ast_str* pvt_str_state_ex(const struct pvt* pvt)
|
|
{
|
|
struct ast_str* buf = ast_str_create (256);
|
|
const char * state = pvt_state_base(pvt);
|
|
|
|
if(state)
|
|
ast_str_append (&buf, 0, "%s", state);
|
|
else
|
|
{
|
|
if(pvt->ring || PVT_STATE(pvt, chan_count[CALL_STATE_INCOMING]))
|
|
ast_str_append (&buf, 0, "Ring ");
|
|
|
|
if(pvt->dialing ||
|
|
(PVT_STATE(pvt, chan_count[CALL_STATE_INIT])
|
|
+
|
|
PVT_STATE(pvt, chan_count[CALL_STATE_DIALING])
|
|
+
|
|
PVT_STATE(pvt, chan_count[CALL_STATE_ALERTING])) > 0)
|
|
ast_str_append (&buf, 0, "Dialing ");
|
|
|
|
if(pvt->cwaiting || PVT_STATE(pvt, chan_count[CALL_STATE_WAITING]))
|
|
ast_str_append (&buf, 0, "Waiting ");
|
|
|
|
if(PVT_STATE(pvt, chan_count[CALL_STATE_ACTIVE]) > 0)
|
|
ast_str_append (&buf, 0, "Active %u ", PVT_STATE(pvt, chan_count[CALL_STATE_ACTIVE]));
|
|
|
|
if(PVT_STATE(pvt, chan_count[CALL_STATE_ONHOLD]) > 0)
|
|
ast_str_append (&buf, 0, "Held %u ", PVT_STATE(pvt, chan_count[CALL_STATE_ONHOLD]));
|
|
|
|
if(pvt->incoming_sms_index != -1U)
|
|
ast_str_append (&buf, 0, "Incoming SMS ");
|
|
|
|
if(pvt->outgoing_sms)
|
|
ast_str_append (&buf, 0, "Outgoing SMS");
|
|
|
|
if(ast_str_strlen(buf) == 0)
|
|
{
|
|
ast_str_append (&buf, 0, "%s", "Free");
|
|
}
|
|
}
|
|
|
|
if(pvt->desired_state != pvt->current_state)
|
|
ast_str_append (&buf, 0, " %s", dev_state2str_msg(pvt->desired_state));
|
|
|
|
return buf;
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF const char* GSM_regstate2str(int gsm_reg_status)
|
|
{
|
|
static const char * const gsm_states[] = {
|
|
"Not registered, not searching",
|
|
"Registered, home network",
|
|
"Not registered, but searching",
|
|
"Registration denied",
|
|
"Unknown",
|
|
"Registered, roaming",
|
|
};
|
|
return enum2str_def(gsm_reg_status, gsm_states, ITEMS_OF (gsm_states), "Unknown");
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF const char* sys_mode2str(int sys_mode)
|
|
{
|
|
static const char * const sys_modes[] = {
|
|
"No Service",
|
|
"AMPS",
|
|
"CDMA",
|
|
"GSM/GPRS",
|
|
"HDR",
|
|
"WCDMA",
|
|
"GPS",
|
|
};
|
|
|
|
return enum2str_def(sys_mode, sys_modes, ITEMS_OF (sys_modes), "Unknown");
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF const char * sys_submode2str(int sys_submode)
|
|
{
|
|
static const char * const sys_submodes[] = {
|
|
"No service",
|
|
"GSM",
|
|
"GPRS",
|
|
"EDGE",
|
|
"WCDMA",
|
|
"HSDPA",
|
|
"HSUPA",
|
|
"HSDPA and HSUPA",
|
|
};
|
|
|
|
return enum2str_def(sys_submode, sys_submodes, ITEMS_OF (sys_submodes), "Unknown");
|
|
}
|
|
|
|
#/* BUGFIX of https://code.google.com/p/asterisk-chan-dongle/issues/detail?id=118 */
|
|
EXPORT_DEF char* rssi2dBm(int rssi, char * buf, unsigned len)
|
|
{
|
|
#if 0 /* old code */
|
|
if(rssi <= 0)
|
|
{
|
|
snprintf(buf, len, "<= -125 dBm");
|
|
}
|
|
else if(rssi <= 30)
|
|
{
|
|
snprintf(buf, len, "%d dBm", 31 * rssi / 50 - 125);
|
|
}
|
|
else if(rssi == 31)
|
|
{
|
|
snprintf(buf, len, ">= -75 dBm");
|
|
}
|
|
else
|
|
{
|
|
snprintf(buf, len, "unknown");
|
|
}
|
|
#else
|
|
if(rssi <= 0)
|
|
{
|
|
snprintf(buf, len, "<= -113 dBm");
|
|
}
|
|
else if(rssi <= 30)
|
|
{
|
|
snprintf(buf, len, "%d dBm", 2 * rssi - 113);
|
|
}
|
|
else if(rssi == 31)
|
|
{
|
|
snprintf(buf, len, ">= -51 dBm");
|
|
}
|
|
else
|
|
{
|
|
snprintf(buf, len, "unknown or unmeasurable");
|
|
}
|
|
#endif
|
|
return buf;
|
|
}
|
|
|
|
/* Module */
|
|
|
|
#/* */
|
|
EXPORT_DEF void pvt_dsp_setup(struct pvt * pvt, const char * id, dc_dtmf_setting_t dtmf_new)
|
|
{
|
|
/* first remove dsp if off or changed */
|
|
if(dtmf_new != CONF_SHARED(pvt, dtmf))
|
|
{
|
|
if(pvt->dsp)
|
|
{
|
|
ast_dsp_free(pvt->dsp);
|
|
pvt->dsp = NULL;
|
|
}
|
|
}
|
|
|
|
/* wake up and initialize dsp */
|
|
if(dtmf_new != DC_DTMF_SETTING_OFF)
|
|
{
|
|
pvt->dsp = ast_dsp_new();
|
|
if(pvt->dsp)
|
|
{
|
|
int digitmode = DSP_DIGITMODE_DTMF;
|
|
if(dtmf_new == DC_DTMF_SETTING_RELAX)
|
|
digitmode |= DSP_DIGITMODE_RELAXDTMF;
|
|
|
|
ast_dsp_set_features(pvt->dsp, DSP_FEATURE_DIGIT_DETECT);
|
|
ast_dsp_set_digitmode(pvt->dsp, digitmode);
|
|
}
|
|
else
|
|
{
|
|
ast_log(LOG_ERROR, "[%s] Can't setup dsp for dtmf detection, ignored\n", id);
|
|
}
|
|
}
|
|
pvt->real_dtmf = dtmf_new;
|
|
}
|
|
|
|
static struct pvt * pvt_create(const pvt_config_t * settings)
|
|
{
|
|
struct pvt * pvt = ast_calloc (1, sizeof (*pvt));
|
|
if(pvt)
|
|
{
|
|
ast_mutex_init (&pvt->lock);
|
|
|
|
AST_LIST_HEAD_INIT_NOLOCK (&pvt->at_queue);
|
|
AST_LIST_HEAD_INIT_NOLOCK (&pvt->chans);
|
|
pvt->sys_chan.pvt = pvt;
|
|
pvt->sys_chan.state = CALL_STATE_RELEASED;
|
|
|
|
pvt->monitor_thread = AST_PTHREADT_NULL;
|
|
pvt->audio_fd = -1;
|
|
pvt->data_fd = -1;
|
|
pvt->timeout = DATA_READ_TIMEOUT;
|
|
pvt->gsm_reg_status = -1;
|
|
pvt->incoming_sms_index = -1U;
|
|
|
|
ast_copy_string (pvt->provider_name, "NONE", sizeof (pvt->provider_name));
|
|
ast_copy_string (pvt->subscriber_number, "Unknown", sizeof (pvt->subscriber_number));
|
|
pvt->has_subscriber_number = 0;
|
|
|
|
pvt->desired_state = SCONFIG(settings, initstate);
|
|
|
|
pvt_dsp_setup(pvt, UCONFIG(settings, id), SCONFIG(settings, dtmf));
|
|
|
|
/* and copy settings */
|
|
memcpy(&pvt->settings, settings, sizeof(pvt->settings));
|
|
return pvt;
|
|
}
|
|
else
|
|
{
|
|
ast_log (LOG_ERROR, "[%s] Skipping device: Error allocating memory\n", UCONFIG(settings, id));
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#/* */
|
|
static int pvt_time4restate(const struct pvt * pvt)
|
|
{
|
|
if(pvt->desired_state != pvt->current_state)
|
|
{
|
|
if(pvt->restart_time == RESTATE_TIME_NOW || (PVT_NO_CHANS(pvt) && !pvt->outgoing_sms && pvt->incoming_sms_index == -1U))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#/* */
|
|
EXPORT_DEF void pvt_try_restate(struct pvt * pvt)
|
|
{
|
|
if(pvt_time4restate(pvt))
|
|
{
|
|
pvt->restart_time = RESTATE_TIME_NOW;
|
|
discovery_restart(gpublic);
|
|
}
|
|
}
|
|
|
|
#/* assume caller hold lock */
|
|
static int pvt_reconfigure(struct pvt * pvt, const pvt_config_t * settings, restate_time_t when)
|
|
{
|
|
int rv = 0;
|
|
|
|
if(SCONFIG(settings, initstate) == DEV_STATE_REMOVED)
|
|
{
|
|
/* handle later, in one place */
|
|
pvt->must_remove = 1;
|
|
}
|
|
else
|
|
{
|
|
/* check what changes require starting or stopping */
|
|
if(pvt->desired_state != SCONFIG(settings, initstate)) {
|
|
pvt->desired_state = SCONFIG(settings, initstate);
|
|
|
|
rv = pvt_time4restate(pvt);
|
|
pvt->restart_time = rv ? RESTATE_TIME_NOW : when;
|
|
}
|
|
|
|
/* check what config changes require restaring */
|
|
else if(strcmp(UCONFIG(settings, audio_tty), CONF_UNIQ(pvt, audio_tty))
|
|
|| strcmp(UCONFIG(settings, data_tty), CONF_UNIQ(pvt, data_tty))
|
|
|| strcmp(UCONFIG(settings, imei), CONF_UNIQ(pvt, imei))
|
|
|| strcmp(UCONFIG(settings, imsi), CONF_UNIQ(pvt, imsi))
|
|
|| SCONFIG(settings, u2diag) != CONF_SHARED(pvt, u2diag)
|
|
|| SCONFIG(settings, resetdongle) != CONF_SHARED(pvt, resetdongle)
|
|
|| SCONFIG(settings, callwaiting) != CONF_SHARED(pvt, callwaiting))
|
|
{
|
|
/* TODO: schedule restart */
|
|
pvt->desired_state = DEV_STATE_RESTARTED;
|
|
|
|
rv = pvt_time4restate(pvt);
|
|
pvt->restart_time = rv ? RESTATE_TIME_NOW : when;
|
|
}
|
|
|
|
pvt_dsp_setup(pvt, UCONFIG(settings, id), SCONFIG(settings, dtmf));
|
|
|
|
/* and copy settings */
|
|
memcpy(&pvt->settings, settings, sizeof(pvt->settings));
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
#/* */
|
|
static int reload_config(public_state_t * state, int recofigure, restate_time_t when, unsigned * reload_immediality)
|
|
{
|
|
struct ast_config * cfg;
|
|
const char * cat;
|
|
struct ast_flags config_flags = { 0 };
|
|
struct dc_sconfig config_defaults;
|
|
pvt_config_t settings;
|
|
int err;
|
|
struct pvt * pvt;
|
|
unsigned reload_now = 0;
|
|
|
|
if ((cfg = ast_config_load (CONFIG_FILE, config_flags)) == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
/* read global config */
|
|
dc_gconfig_fill(cfg, "general", &state->global_settings);
|
|
|
|
/* read defaults */
|
|
dc_sconfig_fill_defaults(&config_defaults);
|
|
dc_sconfig_fill(cfg, "defaults", &config_defaults);
|
|
|
|
/* FIXME: deadlock avoid ? */
|
|
AST_RWLIST_RDLOCK(&state->devices);
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock(&pvt->lock);
|
|
pvt->must_remove = 1;
|
|
ast_mutex_unlock(&pvt->lock);
|
|
}
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
|
|
/* now load devices */
|
|
for (cat = ast_category_browse (cfg, NULL); cat; cat = ast_category_browse (cfg, cat))
|
|
{
|
|
if (strcasecmp (cat, "general") && strcasecmp (cat, "defaults"))
|
|
{
|
|
err = dc_config_fill(cfg, cat, &config_defaults, &settings);
|
|
if(!err)
|
|
{
|
|
pvt = find_device(UCONFIG(&settings, id));
|
|
if(pvt)
|
|
{
|
|
if(!recofigure)
|
|
{
|
|
ast_log (LOG_ERROR, "device %s already exists, duplicate in config file\n", cat);
|
|
}
|
|
else
|
|
{
|
|
pvt->must_remove = 0;
|
|
reload_now += pvt_reconfigure(pvt, &settings, when);
|
|
}
|
|
ast_mutex_unlock(&pvt->lock);
|
|
}
|
|
else
|
|
{
|
|
/* new device */
|
|
if(SCONFIG(&settings, initstate) == DEV_STATE_REMOVED)
|
|
{
|
|
ast_log (LOG_NOTICE, "Skipping device %s as disabled\n", cat);
|
|
}
|
|
else
|
|
{
|
|
pvt = pvt_create(&settings);
|
|
if(pvt)
|
|
{
|
|
/* FIXME: deadlock avoid ? */
|
|
AST_RWLIST_WRLOCK(&state->devices);
|
|
AST_RWLIST_INSERT_TAIL(&state->devices, pvt, entry);
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
reload_now++;
|
|
|
|
ast_log (LOG_NOTICE, "[%s] Loaded device\n", PVT_ID(pvt));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ast_config_destroy (cfg);
|
|
|
|
/* FIXME: deadlock avoid ? */
|
|
/* schedule removal of devices not listed in config file or disabled */
|
|
AST_RWLIST_RDLOCK(&state->devices);
|
|
AST_RWLIST_TRAVERSE(&state->devices, pvt, entry)
|
|
{
|
|
ast_mutex_lock(&pvt->lock);
|
|
if(pvt->must_remove)
|
|
{
|
|
pvt->desired_state = DEV_STATE_REMOVED;
|
|
if(pvt_time4restate(pvt))
|
|
{
|
|
pvt->restart_time = RESTATE_TIME_NOW;
|
|
reload_now++;
|
|
}
|
|
else
|
|
pvt->restart_time = when;
|
|
}
|
|
ast_mutex_unlock(&pvt->lock);
|
|
}
|
|
AST_RWLIST_UNLOCK (&state->devices);
|
|
|
|
if(reload_immediality)
|
|
*reload_immediality = reload_now;
|
|
return 0;
|
|
}
|
|
|
|
|
|
#/* */
|
|
static void devices_destroy(public_state_t * state)
|
|
{
|
|
struct pvt * pvt;
|
|
|
|
/* Destroy the device list */
|
|
AST_RWLIST_WRLOCK(&state->devices);
|
|
while((pvt = AST_RWLIST_REMOVE_HEAD(&state->devices, entry)))
|
|
{
|
|
pvt_destroy(pvt);
|
|
}
|
|
AST_RWLIST_UNLOCK(&state->devices);
|
|
}
|
|
|
|
|
|
static int load_module()
|
|
{
|
|
int rv;
|
|
|
|
gpublic = ast_calloc(1, sizeof(*gpublic));
|
|
if(gpublic)
|
|
{
|
|
pdiscovery_init();
|
|
rv = public_state_init(gpublic);
|
|
if(rv != AST_MODULE_LOAD_SUCCESS)
|
|
ast_free(gpublic);
|
|
}
|
|
else
|
|
{
|
|
ast_log (LOG_ERROR, "Unable to allocate global state structure\n");
|
|
rv = AST_MODULE_LOAD_DECLINE;
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
#/* */
|
|
static int public_state_init(struct public_state * state)
|
|
{
|
|
int rv = AST_MODULE_LOAD_DECLINE;
|
|
|
|
AST_RWLIST_HEAD_INIT(&state->devices);
|
|
ast_mutex_init(&state->discovery_lock);
|
|
|
|
state->discovery_thread = AST_PTHREADT_NULL;
|
|
|
|
if(reload_config(state, 0, RESTATE_TIME_NOW, NULL) == 0)
|
|
{
|
|
rv = AST_MODULE_LOAD_FAILURE;
|
|
if(discovery_restart(state) == 0)
|
|
{
|
|
|
|
/* set preferred capabilities */
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
if (!(channel_tech.capabilities = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT))) {
|
|
return AST_MODULE_LOAD_FAILURE;
|
|
}
|
|
ast_format_cap_append(channel_tech.capabilities, ast_format_slin, 0);
|
|
#elif ASTERISK_VERSION_NUM >= 100000 /* 10-13 */
|
|
ast_format_set(&chan_dongle_format, AST_FORMAT_SLINEAR, 0);
|
|
# if ASTERISK_VERSION_NUM >= 120000 /* 12+ */
|
|
if (!(channel_tech.capabilities = ast_format_cap_alloc(0))) {
|
|
return AST_MODULE_LOAD_FAILURE;
|
|
}
|
|
# else
|
|
if (!(channel_tech.capabilities = ast_format_cap_alloc())) {
|
|
return AST_MODULE_LOAD_FAILURE;
|
|
}
|
|
# endif
|
|
ast_format_cap_add(channel_tech.capabilities, &chan_dongle_format);
|
|
chan_dongle_format_cap = channel_tech.capabilities;
|
|
#endif /* ^10-13 */
|
|
|
|
/* register our channel type */
|
|
if(ast_channel_register(&channel_tech) == 0)
|
|
{
|
|
if (smsdb_init() != 0) {
|
|
ast_log(LOG_ERROR, "Unable to init smsdb\n");
|
|
}
|
|
cli_register();
|
|
|
|
app_register();
|
|
manager_register();
|
|
|
|
return AST_MODULE_LOAD_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
ao2_cleanup(channel_tech.capabilities);
|
|
channel_tech.capabilities = NULL;
|
|
#elif ASTERISK_VERSION_NUM >= 100000 /* 10-13 */
|
|
channel_tech.capabilities = ast_format_cap_destroy(channel_tech.capabilities);
|
|
#endif /* ^10-13 */
|
|
ast_log (LOG_ERROR, "Unable to register channel class %s\n", channel_tech.type);
|
|
}
|
|
discovery_stop(state);
|
|
}
|
|
else
|
|
{
|
|
ast_log (LOG_ERROR, "Unable to create discovery thread\n");
|
|
}
|
|
devices_destroy(state);
|
|
}
|
|
else
|
|
{
|
|
ast_log (LOG_ERROR, "Errors reading config file " CONFIG_FILE ", Not loading module\n");
|
|
}
|
|
|
|
ast_mutex_destroy(&state->discovery_lock);
|
|
AST_RWLIST_HEAD_DESTROY(&state->devices);
|
|
|
|
return rv;
|
|
}
|
|
|
|
#/* */
|
|
static void public_state_fini(struct public_state * state)
|
|
{
|
|
/* First, take us out of the channel loop */
|
|
ast_channel_unregister (&channel_tech);
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
ao2_cleanup(channel_tech.capabilities);
|
|
channel_tech.capabilities = NULL;
|
|
#elif ASTERISK_VERSION_NUM >= 100000 /* 10-13 */
|
|
channel_tech.capabilities = ast_format_cap_destroy(channel_tech.capabilities);
|
|
#endif /* ^10-13 */
|
|
|
|
/* Unregister the CLI & APP & MANAGER */
|
|
|
|
manager_unregister();
|
|
|
|
app_unregister();
|
|
|
|
cli_unregister();
|
|
|
|
discovery_stop(state);
|
|
devices_destroy(state);
|
|
|
|
ast_mutex_destroy(&state->discovery_lock);
|
|
AST_RWLIST_HEAD_DESTROY(&state->devices);
|
|
}
|
|
|
|
static int unload_module()
|
|
{
|
|
|
|
public_state_fini(gpublic);
|
|
pdiscovery_fini();
|
|
|
|
ast_free(gpublic);
|
|
smsdb_atexit();
|
|
gpublic = NULL;
|
|
return 0;
|
|
}
|
|
|
|
|
|
#/* */
|
|
EXPORT_DEF void pvt_reload(restate_time_t when)
|
|
{
|
|
unsigned dev_reload = 0;
|
|
reload_config(gpublic, 1, when, &dev_reload);
|
|
if(dev_reload > 0)
|
|
discovery_restart(gpublic);
|
|
}
|
|
|
|
#/* */
|
|
static int reload_module()
|
|
{
|
|
pvt_reload(RESTATE_TIME_GRACEFULLY);
|
|
return 0;
|
|
}
|
|
|
|
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, MODULE_DESCRIPTION,
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
.support_level = AST_MODULE_SUPPORT_EXTENDED,
|
|
#endif /* ^13+ */
|
|
.load = load_module,
|
|
.unload = unload_module,
|
|
.reload = reload_module,
|
|
#if ASTERISK_VERSION_NUM >= 130000 /* 13+ */
|
|
.load_pri = AST_MODPRI_CHANNEL_DRIVER,
|
|
#endif /* ^13+ */
|
|
);
|
|
|
|
//AST_MODULE_INFO_STANDARD (ASTERISK_GPL_KEY, MODULE_DESCRIPTION);
|
|
|
|
EXPORT_DEF struct ast_module* self_module(void)
|
|
{
|
|
return ast_module_info->self;
|
|
}
|