dahdi-linux/drivers/dahdi/xpp/card_fxo.c
Shaun Ruffell bf3fe05dfb wct4xxp: Moving the transmit short detection behind debug module param.
This needs some more testing before it's on by default.  If the card is
otherwise functioning, these messages may be confusing to the user.  If
the card is not functioning, the driver can be reloaded with debug to
check for this condition.

Signed-off-by: Shaun Ruffell <sruffell@digium.com>

git-svn-id: http://svn.asterisk.org/svn/dahdi/linux/trunk@9205 a0bf4364-ded3-4de4-8d8a-66a801d63aff
2010-08-27 21:59:27 +00:00

1428 lines
38 KiB
C

/*
* Written by Oron Peled <oron@actcom.co.il>
* Copyright (C) 2004-2006, Xorcom
*
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include "xpd.h"
#include "xproto.h"
#include "xpp_dahdi.h"
#include "card_fxo.h"
#include "dahdi_debug.h"
#include "xbus-core.h"
static const char rcsid[] = "$Id$";
static DEF_PARM(int, debug, 0, 0644, "Print DBG statements");
static DEF_PARM(uint, poll_battery_interval, 500, 0644, "Poll battery interval in milliseconds (0 - disable)");
#ifdef WITH_METERING
static DEF_PARM(uint, poll_metering_interval, 500, 0644, "Poll metering interval in milliseconds (0 - disable)");
#endif
static DEF_PARM(int, ring_debounce, 50, 0644, "Number of ticks to debounce a false RING indication");
static DEF_PARM(int, caller_id_style, 0, 0444, "Caller-Id detection style: 0 - [BELL], 1 - [ETSI_FSK], 2 - [ETSI_DTMF]");
static DEF_PARM(int, power_denial_safezone, 650, 0644, "msec after offhook to ignore power-denial ( (0 - disable power-denial)");
static DEF_PARM(int, power_denial_minlen, 80, 0644, "Minimal detected power-denial length (msec) (0 - disable power-denial)");
static DEF_PARM(uint, battery_threshold, 3, 0644, "Minimum voltage that shows there is battery");
static DEF_PARM(uint, battery_debounce, 1000, 0644, "Minimum interval (msec) for detection of battery off");
enum cid_style {
CID_STYLE_BELL = 0, /* E.g: US (Bellcore) */
CID_STYLE_ETSI_FSK = 1, /* E.g: UK (British Telecom) */
CID_STYLE_ETSI_DTMF = 2, /* E.g: DK, Russia */
};
/* Signaling is opposite (fxs signalling for fxo card) */
#if 1
#define FXO_DEFAULT_SIGCAP (DAHDI_SIG_FXSKS | DAHDI_SIG_FXSLS)
#else
#define FXO_DEFAULT_SIGCAP (DAHDI_SIG_SF)
#endif
enum fxo_leds {
LED_GREEN,
LED_RED,
};
#define NUM_LEDS 2
#define DELAY_UNTIL_DIALTONE 3000
/*
* Minimum duration for polarity reversal detection (in ticks)
* Should be longer than the time to detect a ring, so voltage
* fluctuation during ring won't trigger false detection.
*/
#define POLREV_THRESHOLD 200
#define POWER_DENIAL_CURRENT 3
#define POWER_DENIAL_DELAY 2500 /* ticks */
/* Shortcuts */
#define DAA_WRITE 1
#define DAA_READ 0
#define DAA_DIRECT_REQUEST(xbus,xpd,port,writing,reg,dL) \
xpp_register_request((xbus), (xpd), (port), (writing), (reg), 0, 0, (dL), 0, 0, 0)
/*---------------- FXO Protocol Commands ----------------------------------*/
static /* 0x0F */ DECLARE_CMD(FXO, XPD_STATE, bool on);
static bool fxo_packet_is_valid(xpacket_t *pack);
static void fxo_packet_dump(const char *msg, xpacket_t *pack);
#ifdef CONFIG_PROC_FS
static int proc_fxo_info_read(char *page, char **start, off_t off, int count, int *eof, void *data);
#ifdef WITH_METERING
static int proc_xpd_metering_read(char *page, char **start, off_t off, int count, int *eof, void *data);
#endif
#endif
static void dahdi_report_battery(xpd_t *xpd, lineno_t chan);
#define PROC_REGISTER_FNAME "slics"
#define PROC_FXO_INFO_FNAME "fxo_info"
#ifdef WITH_METERING
#define PROC_METERING_FNAME "metering_read"
#endif
#define REG_DAA_CONTROL1 0x05 /* 5 - DAA Control 1 */
#define REG_DAA_CONTROL1_OH BIT(0) /* Off-Hook. */
#define REG_DAA_CONTROL1_ONHM BIT(3) /* On-Hook Line Monitor */
#define DAA_REG_METERING 0x11 /* 17 */
#define DAA_REG_CURRENT 0x1C /* 28 */
#define DAA_REG_VBAT 0x1D /* 29 */
enum battery_state {
BATTERY_UNKNOWN = 0,
BATTERY_ON = 1,
BATTERY_OFF = -1
};
enum polarity_state {
POL_UNKNOWN = 0,
POL_POSITIVE = 1,
POL_NEGATIVE = -1
};
enum power_state {
POWER_UNKNOWN = 0,
POWER_ON = 1,
POWER_OFF = -1
};
struct FXO_priv_data {
#ifdef WITH_METERING
struct proc_dir_entry *meteringfile;
#endif
struct proc_dir_entry *fxo_info;
uint poll_counter;
signed char battery_voltage[CHANNELS_PERXPD];
signed char battery_current[CHANNELS_PERXPD];
enum battery_state battery[CHANNELS_PERXPD];
ushort nobattery_debounce[CHANNELS_PERXPD];
enum polarity_state polarity[CHANNELS_PERXPD];
ushort polarity_debounce[CHANNELS_PERXPD];
enum power_state power[CHANNELS_PERXPD];
ushort power_denial_delay[CHANNELS_PERXPD];
ushort power_denial_length[CHANNELS_PERXPD];
ushort power_denial_safezone[CHANNELS_PERXPD];
xpp_line_t cidfound; /* 0 - OFF, 1 - ON */
unsigned int cidtimer[CHANNELS_PERXPD];
xpp_line_t ledstate[NUM_LEDS]; /* 0 - OFF, 1 - ON */
xpp_line_t ledcontrol[NUM_LEDS]; /* 0 - OFF, 1 - ON */
int led_counter[NUM_LEDS][CHANNELS_PERXPD];
atomic_t ring_debounce[CHANNELS_PERXPD];
#ifdef WITH_METERING
uint metering_count[CHANNELS_PERXPD];
xpp_line_t metering_tone_state;
#endif
};
/*
* LED counter values:
* n>1 : BLINK every n'th tick
*/
#define LED_COUNTER(priv,pos,color) ((priv)->led_counter[color][pos])
#define IS_BLINKING(priv,pos,color) (LED_COUNTER(priv,pos,color) > 0)
#define MARK_BLINK(priv,pos,color,t) ((priv)->led_counter[color][pos] = (t))
#define MARK_OFF(priv,pos,color) do { BIT_CLR((priv)->ledcontrol[color],(pos)); MARK_BLINK((priv),(pos),(color),0); } while(0)
#define MARK_ON(priv,pos,color) do { BIT_SET((priv)->ledcontrol[color],(pos)); MARK_BLINK((priv),(pos),(color),0); } while(0)
#define LED_BLINK_RING (1000/8) /* in ticks */
/*---------------- FXO: Static functions ----------------------------------*/
static const char *power2str(enum power_state pw)
{
switch(pw) {
case POWER_UNKNOWN: return "UNKNOWN";
case POWER_OFF: return "OFF";
case POWER_ON: return "ON";
}
return NULL;
}
static void power_change(xpd_t *xpd, int portno, enum power_state pw)
{
struct FXO_priv_data *priv;
priv = xpd->priv;
LINE_DBG(SIGNAL, xpd, portno, "power: %s -> %s\n",
power2str(priv->power[portno]),
power2str(pw));
priv->power[portno] = pw;
}
static void reset_battery_readings(xpd_t *xpd, lineno_t pos)
{
struct FXO_priv_data *priv = xpd->priv;
priv->nobattery_debounce[pos] = 0;
priv->power_denial_delay[pos] = 0;
power_change(xpd, pos, POWER_UNKNOWN);
}
static const int led_register_mask[] = { BIT(7), BIT(6), BIT(5) };
/*
* LED control is done via DAA register 0x20
*/
static int do_led(xpd_t *xpd, lineno_t chan, byte which, bool on)
{
int ret = 0;
struct FXO_priv_data *priv;
xbus_t *xbus;
byte value;
BUG_ON(!xpd);
xbus = xpd->xbus;
priv = xpd->priv;
which = which % NUM_LEDS;
if(IS_SET(xpd->digital_outputs, chan) || IS_SET(xpd->digital_inputs, chan))
goto out;
if(chan == PORT_BROADCAST) {
priv->ledstate[which] = (on) ? ~0 : 0;
} else {
if(on) {
BIT_SET(priv->ledstate[which], chan);
} else {
BIT_CLR(priv->ledstate[which], chan);
}
}
value = 0;
value |= ((BIT(5) | BIT(6) | BIT(7)) & ~led_register_mask[which]);
value |= (on) ? BIT(0) : 0;
value |= (on) ? BIT(1) : 0;
LINE_DBG(LEDS, xpd, chan, "LED: which=%d -- %s\n", which, (on) ? "on" : "off");
ret = DAA_DIRECT_REQUEST(xbus, xpd, chan, DAA_WRITE, 0x20, value);
out:
return ret;
}
static void handle_fxo_leds(xpd_t *xpd)
{
int i;
unsigned long flags;
const enum fxo_leds colors[] = { LED_GREEN, LED_RED };
enum fxo_leds color;
unsigned int timer_count;
struct FXO_priv_data *priv;
BUG_ON(!xpd);
spin_lock_irqsave(&xpd->lock, flags);
priv = xpd->priv;
timer_count = xpd->timer_count;
for(color = 0; color < ARRAY_SIZE(colors); color++) {
for_each_line(xpd, i) {
if(IS_SET(xpd->digital_outputs, i) || IS_SET(xpd->digital_inputs, i))
continue;
if((xpd->blink_mode & BIT(i)) || IS_BLINKING(priv, i, color)) { // Blinking
int mod_value = LED_COUNTER(priv, i, color);
if(!mod_value)
mod_value = DEFAULT_LED_PERIOD; /* safety value */
// led state is toggled
if((timer_count % mod_value) == 0) {
LINE_DBG(LEDS, xpd, i, "ledstate=%s\n", (IS_SET(priv->ledstate[color], i))?"ON":"OFF");
if(!IS_SET(priv->ledstate[color], i)) {
do_led(xpd, i, color, 1);
} else {
do_led(xpd, i, color, 0);
}
}
} else if(IS_SET(priv->ledcontrol[color], i) && !IS_SET(priv->ledstate[color], i)) {
do_led(xpd, i, color, 1);
} else if(!IS_SET(priv->ledcontrol[color], i) && IS_SET(priv->ledstate[color], i)) {
do_led(xpd, i, color, 0);
}
}
}
spin_unlock_irqrestore(&xpd->lock, flags);
}
static void update_dahdi_ring(xpd_t *xpd, int pos, bool on)
{
BUG_ON(!xpd);
if(caller_id_style == CID_STYLE_BELL)
oht_pcm(xpd, pos, !on);
/*
* We should not spinlock before calling dahdi_hooksig() as
* it may call back into our xpp_hooksig() and cause
* a nested spinlock scenario
*/
notify_rxsig(xpd, pos, (on) ? DAHDI_RXSIG_RING : DAHDI_RXSIG_OFFHOOK);
}
static void mark_ring(xpd_t *xpd, lineno_t pos, bool on, bool update_dahdi)
{
struct FXO_priv_data *priv;
priv = xpd->priv;
BUG_ON(!priv);
atomic_set(&priv->ring_debounce[pos], 0); /* Stop debouncing */
/*
* We don't want to check battery during ringing
* due to voltage fluctuations.
*/
reset_battery_readings(xpd, pos);
if(on && !xpd->ringing[pos]) {
LINE_DBG(SIGNAL, xpd, pos, "START\n");
xpd->ringing[pos] = 1;
priv->cidtimer[pos] = xpd->timer_count;
MARK_BLINK(priv, pos, LED_GREEN, LED_BLINK_RING);
if(update_dahdi)
update_dahdi_ring(xpd, pos, on);
} else if(!on && xpd->ringing[pos]) {
LINE_DBG(SIGNAL, xpd, pos, "STOP\n");
xpd->ringing[pos] = 0;
priv->cidtimer[pos] = xpd->timer_count;
if(IS_BLINKING(priv, pos, LED_GREEN))
MARK_BLINK(priv, pos, LED_GREEN, 0);
if(update_dahdi)
update_dahdi_ring(xpd, pos, on);
}
}
static int do_sethook(xpd_t *xpd, int pos, bool to_offhook)
{
unsigned long flags;
xbus_t *xbus;
struct FXO_priv_data *priv;
int ret = 0;
byte value;
BUG_ON(!xpd);
BUG_ON(xpd->direction == TO_PHONE); // We can SETHOOK state only on PSTN
xbus = xpd->xbus;
priv = xpd->priv;
BUG_ON(!priv);
if(priv->battery[pos] != BATTERY_ON && to_offhook) {
LINE_NOTICE(xpd, pos, "Cannot take offhook while battery is off!\n");
return -EINVAL;
}
spin_lock_irqsave(&xpd->lock, flags);
mark_ring(xpd, pos, 0, 0); // No more rings
value = REG_DAA_CONTROL1_ONHM; /* Bit 3 is for CID */
if(to_offhook)
value |= REG_DAA_CONTROL1_OH;
LINE_DBG(SIGNAL, xpd, pos, "SETHOOK: value=0x%02X %s\n", value, (to_offhook)?"OFFHOOK":"ONHOOK");
if(to_offhook)
MARK_ON(priv, pos, LED_GREEN);
else
MARK_OFF(priv, pos, LED_GREEN);
ret = DAA_DIRECT_REQUEST(xbus, xpd, pos, DAA_WRITE, REG_DAA_CONTROL1, value);
mark_offhook(xpd, pos, to_offhook);
if(caller_id_style != CID_STYLE_ETSI_DTMF)
oht_pcm(xpd, pos, 0);
#ifdef WITH_METERING
priv->metering_count[pos] = 0;
priv->metering_tone_state = 0L;
DAA_DIRECT_REQUEST(xbus, xpd, pos, DAA_WRITE, DAA_REG_METERING, 0x2D);
#endif
reset_battery_readings(xpd, pos); /* unstable during hook changes */
if(to_offhook) {
priv->power_denial_safezone[pos] = power_denial_safezone;
} else {
priv->power_denial_length[pos] = 0;
priv->power_denial_safezone[pos] = 0;
}
priv->cidtimer[pos] = xpd->timer_count;
spin_unlock_irqrestore(&xpd->lock, flags);
return ret;
}
/*---------------- FXO: Methods -------------------------------------------*/
static void fxo_proc_remove(xbus_t *xbus, xpd_t *xpd)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
XPD_DBG(PROC, xpd, "\n");
#ifdef CONFIG_PROC_FS
#ifdef WITH_METERING
if(priv->meteringfile) {
XPD_DBG(PROC, xpd, "Removing xpd metering tone file\n");
priv->meteringfile->data = NULL;
remove_proc_entry(PROC_METERING_FNAME, xpd->proc_xpd_dir);
priv->meteringfile = NULL;
}
#endif
if(priv->fxo_info) {
XPD_DBG(PROC, xpd, "Removing xpd FXO_INFO file\n");
remove_proc_entry(PROC_FXO_INFO_FNAME, xpd->proc_xpd_dir);
priv->fxo_info = NULL;
}
#endif
}
static int fxo_proc_create(xbus_t *xbus, xpd_t *xpd)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
#ifdef CONFIG_PROC_FS
XPD_DBG(PROC, xpd, "Creating FXO_INFO file\n");
priv->fxo_info = create_proc_read_entry(PROC_FXO_INFO_FNAME, 0444, xpd->proc_xpd_dir, proc_fxo_info_read, xpd);
if(!priv->fxo_info) {
XPD_ERR(xpd, "Failed to create proc file '%s'\n", PROC_FXO_INFO_FNAME);
fxo_proc_remove(xbus, xpd);
return -EINVAL;
}
SET_PROC_DIRENTRY_OWNER(priv->fxo_info);
#ifdef WITH_METERING
XPD_DBG(PROC, xpd, "Creating Metering tone file\n");
priv->meteringfile = create_proc_read_entry(PROC_METERING_FNAME, 0444, xpd->proc_xpd_dir,
proc_xpd_metering_read, xpd);
if(!priv->meteringfile) {
XPD_ERR(xpd, "Failed to create proc file '%s'\n", PROC_METERING_FNAME);
fxo_proc_remove(xbus, xpd);
return -EINVAL;
}
SET_PROC_DIRENTRY_OWNER(priv->meteringfile);
#endif
#endif
return 0;
}
static xpd_t *FXO_card_new(xbus_t *xbus, int unit, int subunit, const xproto_table_t *proto_table,
byte subtype, int subunits, int subunit_ports, bool to_phone)
{
xpd_t *xpd = NULL;
int channels;
if(to_phone) {
XBUS_NOTICE(xbus,
"XPD=%d%d: try to instanciate FXO with reverse direction\n",
unit, subunit);
return NULL;
}
if(subtype == 2)
channels = min(2, subunit_ports);
else
channels = min(8, subunit_ports);
xpd = xpd_alloc(xbus, unit, subunit, subtype, subunits, sizeof(struct FXO_priv_data), proto_table, channels);
if(!xpd)
return NULL;
xpd->direction = TO_PSTN;
xpd->type_name = "FXO";
if(fxo_proc_create(xbus, xpd) < 0)
goto err;
return xpd;
err:
xpd_free(xpd);
return NULL;
}
static int FXO_card_init(xbus_t *xbus, xpd_t *xpd)
{
struct FXO_priv_data *priv;
int i;
BUG_ON(!xpd);
priv = xpd->priv;
// Hanghup all lines
for_each_line(xpd, i) {
do_sethook(xpd, i, 0);
priv->polarity[i] = POL_UNKNOWN; /* will be updated on next battery sample */
priv->polarity_debounce[i] = 0;
priv->battery[i] = BATTERY_UNKNOWN; /* will be updated on next battery sample */
priv->power[i] = POWER_UNKNOWN; /* will be updated on next battery sample */
if(caller_id_style == CID_STYLE_ETSI_DTMF)
oht_pcm(xpd, i, 1);
}
XPD_DBG(GENERAL, xpd, "done\n");
for_each_line(xpd, i) {
do_led(xpd, i, LED_GREEN, 0);
}
for_each_line(xpd, i) {
do_led(xpd, i, LED_GREEN, 1);
msleep(50);
}
for_each_line(xpd, i) {
do_led(xpd, i, LED_GREEN, 0);
msleep(50);
}
CALL_XMETHOD(card_pcm_recompute, xbus, xpd, 0);
return 0;
}
static int FXO_card_remove(xbus_t *xbus, xpd_t *xpd)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
XPD_DBG(GENERAL, xpd, "\n");
fxo_proc_remove(xbus, xpd);
return 0;
}
static int FXO_card_dahdi_preregistration(xpd_t *xpd, bool on)
{
xbus_t *xbus;
struct FXO_priv_data *priv;
int i;
unsigned int timer_count;
BUG_ON(!xpd);
xbus = xpd->xbus;
BUG_ON(!xbus);
priv = xpd->priv;
BUG_ON(!priv);
timer_count = xpd->timer_count;
XPD_DBG(GENERAL, xpd, "%s\n", (on)?"ON":"OFF");
for_each_line(xpd, i) {
struct dahdi_chan *cur_chan = XPD_CHAN(xpd, i);
XPD_DBG(GENERAL, xpd, "setting FXO channel %d\n", i);
snprintf(cur_chan->name, MAX_CHANNAME, "XPP_FXO/%02d/%1d%1d/%d",
xbus->num, xpd->addr.unit, xpd->addr.subunit, i);
cur_chan->chanpos = i + 1;
cur_chan->pvt = xpd;
cur_chan->sigcap = FXO_DEFAULT_SIGCAP;
}
for_each_line(xpd, i) {
MARK_ON(priv, i, LED_GREEN);
msleep(4);
MARK_ON(priv, i, LED_RED);
}
for_each_line(xpd, i) {
priv->cidtimer[i] = timer_count;
}
return 0;
}
static int FXO_card_dahdi_postregistration(xpd_t *xpd, bool on)
{
xbus_t *xbus;
struct FXO_priv_data *priv;
int i;
BUG_ON(!xpd);
xbus = xpd->xbus;
BUG_ON(!xbus);
priv = xpd->priv;
BUG_ON(!priv);
XPD_DBG(GENERAL, xpd, "%s\n", (on)?"ON":"OFF");
for_each_line(xpd, i) {
dahdi_report_battery(xpd, i);
MARK_OFF(priv, i, LED_GREEN);
msleep(2);
MARK_OFF(priv, i, LED_RED);
msleep(2);
}
return 0;
}
static int FXO_card_hooksig(xbus_t *xbus, xpd_t *xpd, int pos, enum dahdi_txsig txsig)
{
struct FXO_priv_data *priv;
int ret = 0;
priv = xpd->priv;
BUG_ON(!priv);
LINE_DBG(SIGNAL, xpd, pos, "%s\n", txsig2str(txsig));
BUG_ON(xpd->direction != TO_PSTN);
/* XXX Enable hooksig for FXO XXX */
switch(txsig) {
case DAHDI_TXSIG_START:
case DAHDI_TXSIG_OFFHOOK:
ret = do_sethook(xpd, pos, 1);
break;
case DAHDI_TXSIG_ONHOOK:
ret = do_sethook(xpd, pos, 0);
break;
default:
XPD_NOTICE(xpd, "Can't set tx state to %s (%d)\n",
txsig2str(txsig), txsig);
return -EINVAL;
}
return ret;
}
static void dahdi_report_battery(xpd_t *xpd, lineno_t chan)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
if(SPAN_REGISTERED(xpd)) {
switch(priv->battery[chan]) {
case BATTERY_UNKNOWN:
/* no-op */
break;
case BATTERY_OFF:
LINE_DBG(SIGNAL, xpd, chan, "Send DAHDI_ALARM_RED\n");
dahdi_alarm_channel(XPD_CHAN(xpd, chan), DAHDI_ALARM_RED);
break;
case BATTERY_ON:
LINE_DBG(SIGNAL, xpd, chan, "Send DAHDI_ALARM_NONE\n");
dahdi_alarm_channel(XPD_CHAN(xpd, chan), DAHDI_ALARM_NONE);
break;
}
}
}
static int FXO_card_open(xpd_t *xpd, lineno_t chan)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
return 0;
}
static void poll_battery(xbus_t *xbus, xpd_t *xpd)
{
int i;
for_each_line(xpd, i) {
DAA_DIRECT_REQUEST(xbus, xpd, i, DAA_READ, DAA_REG_VBAT, 0);
}
}
#ifdef WITH_METERING
static void poll_metering(xbus_t *xbus, xpd_t *xpd)
{
int i;
for_each_line(xpd, i) {
if (IS_OFFHOOK(xpd, i))
DAA_DIRECT_REQUEST(xbus, xpd, i, DAA_READ, DAA_REG_METERING, 0);
}
}
#endif
static void handle_fxo_ring(xpd_t *xpd)
{
struct FXO_priv_data *priv;
int i;
priv = xpd->priv;
for_each_line(xpd, i) {
if(atomic_read(&priv->ring_debounce[i]) > 0) {
/* Maybe start ring */
if(atomic_dec_and_test(&priv->ring_debounce[i]))
mark_ring(xpd, i, 1, 1);
} else if (atomic_read(&priv->ring_debounce[i]) < 0) {
/* Maybe stop ring */
if(atomic_inc_and_test(&priv->ring_debounce[i]))
mark_ring(xpd, i, 0, 1);
}
}
}
static void handle_fxo_power_denial(xpd_t *xpd)
{
struct FXO_priv_data *priv;
int i;
if(!power_denial_safezone)
return; /* Ignore power denials */
priv = xpd->priv;
for_each_line(xpd, i) {
if(xpd->ringing[i] || !IS_OFFHOOK(xpd, i)) {
priv->power_denial_delay[i] = 0;
continue;
}
if(priv->power_denial_safezone[i] > 0) {
if(--priv->power_denial_safezone[i] == 0) {
/*
* Poll current, previous answers are meaningless
*/
DAA_DIRECT_REQUEST(xpd->xbus, xpd, i, DAA_READ, DAA_REG_CURRENT, 0);
}
continue;
}
if(priv->power_denial_length[i] > 0) {
priv->power_denial_length[i]--;
if(priv->power_denial_length[i] <= 0) {
/*
* But maybe the FXS started to ring (and the firmware haven't
* detected it yet). This would cause false power denials.
* So we just flag it and schedule more ticks to wait.
*/
LINE_DBG(SIGNAL, xpd, i, "Possible Power Denial Hangup\n");
priv->power_denial_delay[i] = POWER_DENIAL_DELAY;
}
continue;
}
if (priv->power_denial_delay[i] > 0) {
/*
* Ring detection by the firmware takes some time.
* Therefore we delay our decision until we are
* sure that no ring has started during this time.
*/
priv->power_denial_delay[i]--;
if (priv->power_denial_delay[i] <= 0) {
LINE_DBG(SIGNAL, xpd, i, "Power Denial Hangup\n");
priv->power_denial_delay[i] = 0;
/*
* Let Asterisk decide what to do
*/
notify_rxsig(xpd, i, DAHDI_RXSIG_ONHOOK);
}
}
}
}
/*
* For caller-id CID_STYLE_ETSI_DTMF:
* - No indication is passed before the CID
* - We try to detect it and send "fake" polarity reversal.
* - The chan_dahdi.conf should have cidstart=polarity
* - Based on an idea in http://bugs.digium.com/view.php?id=9096
*/
static void check_etsi_dtmf(xpd_t *xpd)
{
struct FXO_priv_data *priv;
int portno;
unsigned int timer_count;
if(!SPAN_REGISTERED(xpd))
return;
priv = xpd->priv;
BUG_ON(!priv);
timer_count = xpd->timer_count;
for_each_line(xpd, portno) {
/* Skip offhook and ringing ports */
if(IS_OFFHOOK(xpd, portno) || xpd->ringing[portno])
continue;
if(IS_SET(priv->cidfound, portno)) {
if(timer_count > priv->cidtimer[portno] + 4000) {
/* reset flags if it's been a while */
priv->cidtimer[portno] = timer_count;
BIT_CLR(priv->cidfound, portno);
LINE_DBG(SIGNAL, xpd, portno, "Reset CID flag\n");
}
continue;
}
if(timer_count > priv->cidtimer[portno] + 400) {
struct dahdi_chan *chan = XPD_CHAN(xpd, portno);
int sample;
int i;
for(i = 0; i < DAHDI_CHUNKSIZE; i++) {
sample = DAHDI_XLAW(chan->readchunk[i], chan);
if(sample > 16000 || sample < -16000) {
priv->cidtimer[portno] = timer_count;
BIT_SET(priv->cidfound, portno);
LINE_DBG(SIGNAL, xpd, portno, "Found DTMF CLIP (%d)\n", i);
dahdi_qevent_lock(chan, DAHDI_EVENT_POLARITY);
break;
}
}
}
}
}
static int FXO_card_tick(xbus_t *xbus, xpd_t *xpd)
{
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
BUG_ON(!priv);
if(poll_battery_interval != 0 && (priv->poll_counter % poll_battery_interval) == 0)
poll_battery(xbus, xpd);
#ifdef WITH_METERING
if(poll_metering_interval != 0 && (priv->poll_counter % poll_metering_interval) == 0)
poll_metering(xbus, xpd);
#endif
handle_fxo_leds(xpd);
handle_fxo_ring(xpd);
handle_fxo_power_denial(xpd);
if(caller_id_style == CID_STYLE_ETSI_DTMF && likely(xpd->card_present))
check_etsi_dtmf(xpd);
priv->poll_counter++;
return 0;
}
#include <dahdi/wctdm_user.h>
/*
* The first register is the ACIM, the other are coefficient registers.
* We define the array size explicitly to track possible inconsistencies
* if the struct is modified.
*/
static const char echotune_regs[sizeof(struct wctdm_echo_coefs)] = {30, 45, 46, 47, 48, 49, 50, 51, 52};
static int FXO_card_ioctl(xpd_t *xpd, int pos, unsigned int cmd, unsigned long arg)
{
int i,ret;
unsigned char echotune_data[ARRAY_SIZE(echotune_regs)];
BUG_ON(!xpd);
if(!XBUS_IS(xpd->xbus, READY))
return -ENODEV;
switch (cmd) {
case WCTDM_SET_ECHOTUNE:
XPD_DBG(GENERAL, xpd, "-- Setting echo registers: \n");
/* first off: check if this span is fxs. If not: -EINVALID */
if (copy_from_user(&echotune_data, (void __user *)arg, sizeof(echotune_data)))
return -EFAULT;
for (i = 0; i < ARRAY_SIZE(echotune_regs); i++) {
XPD_DBG(REGS, xpd, "Reg=0x%02X, data=0x%02X\n", echotune_regs[i], echotune_data[i]);
ret = DAA_DIRECT_REQUEST(xpd->xbus, xpd, pos, DAA_WRITE, echotune_regs[i], echotune_data[i]);
if (ret < 0) {
LINE_NOTICE(xpd, pos, "Couldn't write %0x02X to register %0x02X\n",
echotune_data[i], echotune_regs[i]);
return ret;
}
msleep(1);
}
XPD_DBG(GENERAL, xpd, "-- Set echo registers successfully\n");
break;
case DAHDI_TONEDETECT:
/*
* Asterisk call all span types with this (FXS specific)
* call. Silently ignore it.
*/
LINE_DBG(GENERAL, xpd, pos,
"DAHDI_TONEDETECT (FXO: NOTIMPLEMENTED)\n");
return -ENOTTY;
default:
report_bad_ioctl(THIS_MODULE->name, xpd, pos, cmd);
return -ENOTTY;
}
return 0;
}
/*---------------- FXO: HOST COMMANDS -------------------------------------*/
static /* 0x0F */ HOSTCMD(FXO, XPD_STATE, bool on)
{
int ret = 0;
struct FXO_priv_data *priv;
BUG_ON(!xbus);
BUG_ON(!xpd);
priv = xpd->priv;
BUG_ON(!priv);
XPD_DBG(GENERAL, xpd, "%s\n", (on) ? "on" : "off");
return ret;
}
/*---------------- FXO: Astribank Reply Handlers --------------------------*/
HANDLER_DEF(FXO, SIG_CHANGED)
{
xpp_line_t sig_status = RPACKET_FIELD(pack, FXO, SIG_CHANGED, sig_status);
xpp_line_t sig_toggles = RPACKET_FIELD(pack, FXO, SIG_CHANGED, sig_toggles);
unsigned long flags;
int i;
struct FXO_priv_data *priv;
if(!xpd) {
notify_bad_xpd(__FUNCTION__, xbus, XPACKET_ADDR(pack), cmd->name);
return -EPROTO;
}
priv = xpd->priv;
BUG_ON(!priv);
XPD_DBG(SIGNAL, xpd, "(PSTN) sig_toggles=0x%04X sig_status=0x%04X\n", sig_toggles, sig_status);
spin_lock_irqsave(&xpd->lock, flags);
for_each_line(xpd, i) {
int debounce;
if(IS_SET(sig_toggles, i)) {
if(priv->battery[i] == BATTERY_OFF) {
/*
* With poll_battery_interval==0 we cannot have BATTERY_OFF
* so we won't get here
*/
LINE_NOTICE(xpd, i, "SIG_CHANGED while battery is off. Ignored.\n");
continue;
}
/* First report false ring alarms */
debounce = atomic_read(&priv->ring_debounce[i]);
if(debounce)
LINE_NOTICE(xpd, i, "debounced false ring (only %d ticks)\n", debounce);
/*
* Now set a new ring alarm.
* It will be checked in handle_fxo_ring()
*/
debounce = (IS_SET(sig_status, i)) ? ring_debounce : -ring_debounce;
atomic_set(&priv->ring_debounce[i], debounce);
}
}
spin_unlock_irqrestore(&xpd->lock, flags);
return 0;
}
static void update_battery_voltage(xpd_t *xpd, byte data_low, xportno_t portno)
{
struct FXO_priv_data *priv;
enum polarity_state pol;
int msec;
signed char volts = (signed char)data_low;
priv = xpd->priv;
BUG_ON(!priv);
priv->battery_voltage[portno] = volts;
if(xpd->ringing[portno])
goto ignore_reading; /* ring voltage create false alarms */
if(abs(volts) < battery_threshold) {
/*
* Check for battery voltage fluctuations
*/
if(priv->battery[portno] != BATTERY_OFF) {
int milliseconds;
milliseconds = priv->nobattery_debounce[portno]++ *
poll_battery_interval;
if(milliseconds > battery_debounce) {
LINE_DBG(SIGNAL, xpd, portno, "BATTERY OFF voltage=%d\n", volts);
priv->battery[portno] = BATTERY_OFF;
dahdi_report_battery(xpd, portno);
/* What's the polarity ? */
priv->polarity[portno] = POL_UNKNOWN;
priv->polarity_debounce[portno] = 0;
/* What's the current ? */
power_change(xpd, portno, POWER_UNKNOWN);
/*
* Stop further processing for now
*/
goto ignore_reading;
}
}
} else {
priv->nobattery_debounce[portno] = 0;
if(priv->battery[portno] != BATTERY_ON) {
LINE_DBG(SIGNAL, xpd, portno, "BATTERY ON voltage=%d\n", volts);
priv->battery[portno] = BATTERY_ON;
dahdi_report_battery(xpd, portno);
}
}
#if 0
/*
* Mark FXO ports without battery!
*/
if(priv->battery[portno] != BATTERY_ON)
MARK_ON(priv, portno, LED_RED);
else
MARK_OFF(priv, portno, LED_RED);
#endif
if(priv->battery[portno] != BATTERY_ON) {
priv->polarity[portno] = POL_UNKNOWN; /* What's the polarity ? */
return;
}
/*
* Handle reverse polarity
*/
if(volts == 0)
pol = POL_UNKNOWN;
else if(volts < 0)
pol = POL_NEGATIVE;
else
pol = POL_POSITIVE;
if(priv->polarity[portno] == pol) {
/*
* Same polarity, reset debounce counter
*/
priv->polarity_debounce[portno] = 0;
return;
}
/*
* Track polarity reversals and debounce spikes.
* Only reversals with long duration count.
*/
msec = priv->polarity_debounce[portno]++ * poll_battery_interval;
if (msec >= POLREV_THRESHOLD) {
priv->polarity_debounce[portno] = 0;
if(pol != POL_UNKNOWN && priv->polarity[portno] != POL_UNKNOWN) {
char *polname = NULL;
if(pol == POL_POSITIVE)
polname = "Positive";
else if(pol == POL_NEGATIVE)
polname = "Negative";
else
BUG();
LINE_DBG(SIGNAL, xpd, portno,
"Polarity changed to %s\n", polname);
/*
* Inform dahdi/Asterisk:
* 1. Maybe used for hangup detection during offhook
* 2. In some countries used to report caller-id during onhook
* but before first ring.
*/
if(caller_id_style == CID_STYLE_ETSI_FSK)
oht_pcm(xpd, portno, 1); /* will be cleared on ring/offhook */
if(SPAN_REGISTERED(xpd)) {
LINE_DBG(SIGNAL, xpd, portno,
"Send DAHDI_EVENT_POLARITY: %s\n", polname);
dahdi_qevent_lock(XPD_CHAN(xpd, portno), DAHDI_EVENT_POLARITY);
}
}
priv->polarity[portno] = pol;
}
return;
ignore_reading:
/*
* Reset debounce counters to prevent false alarms
*/
reset_battery_readings(xpd, portno); /* unstable during hook changes */
}
static void update_battery_current(xpd_t *xpd, byte data_low, xportno_t portno)
{
struct FXO_priv_data *priv;
priv = xpd->priv;
BUG_ON(!priv);
priv->battery_current[portno] = data_low;
/*
* During ringing, current is not stable.
* During onhook there should not be current anyway.
*/
if(xpd->ringing[portno] || !IS_OFFHOOK(xpd, portno))
goto ignore_it;
/*
* Power denial with no battery voltage is meaningless
*/
if(priv->battery[portno] != BATTERY_ON)
goto ignore_it;
/* Safe zone after offhook */
if(priv->power_denial_safezone[portno] > 0)
goto ignore_it;
if(data_low < POWER_DENIAL_CURRENT) {
if(priv->power[portno] == POWER_ON) {
power_change(xpd, portno, POWER_OFF);
priv->power_denial_length[portno] = power_denial_minlen;
}
} else {
if(priv->power[portno] != POWER_ON) {
power_change(xpd, portno, POWER_ON);
priv->power_denial_length[portno] = 0;
/* We are now OFFHOOK */
hookstate_changed(xpd, portno, 1);
}
}
return;
ignore_it:
priv->power_denial_delay[portno] = 0;
}
#ifdef WITH_METERING
#define BTD_BIT BIT(0)
static void update_metering_state(xpd_t *xpd, byte data_low, lineno_t portno)
{
struct FXO_priv_data *priv;
bool metering_tone = data_low & BTD_BIT;
bool old_metering_tone;
priv = xpd->priv;
BUG_ON(!priv);
old_metering_tone = IS_SET(priv->metering_tone_state, portno);
LINE_DBG(SIGNAL, xpd, portno, "METERING: %s [dL=0x%X] (%d)\n",
(metering_tone) ? "ON" : "OFF",
data_low, priv->metering_count[portno]);
if(metering_tone && !old_metering_tone) {
/* Rising edge */
priv->metering_count[portno]++;
BIT_SET(priv->metering_tone_state, portno);
} else if(!metering_tone && old_metering_tone)
BIT_CLR(priv->metering_tone_state, portno);
if(metering_tone) {
/* Clear the BTD bit */
data_low &= ~BTD_BIT;
DAA_DIRECT_REQUEST(xpd->xbus, xpd, portno, DAA_WRITE, DAA_REG_METERING, data_low);
}
}
#endif
static int FXO_card_register_reply(xbus_t *xbus, xpd_t *xpd, reg_cmd_t *info)
{
struct FXO_priv_data *priv;
lineno_t portno;
priv = xpd->priv;
BUG_ON(!priv);
portno = info->portnum;
switch(REG_FIELD(info, regnum)) {
case DAA_REG_VBAT:
update_battery_voltage(xpd, REG_FIELD(info, data_low), portno);
break;
case DAA_REG_CURRENT:
update_battery_current(xpd, REG_FIELD(info, data_low), portno);
break;
#ifdef WITH_METERING
case DAA_REG_METERING:
update_metering_state(xpd, REG_FIELD(info, data_low), portno);
break;
#endif
}
LINE_DBG(REGS, xpd, portno, "%c reg_num=0x%X, dataL=0x%X dataH=0x%X\n",
((info->bytes == 3)?'I':'D'),
REG_FIELD(info, regnum),
REG_FIELD(info, data_low),
REG_FIELD(info, data_high));
/* Update /proc info only if reply relate to the last slic read request */
if(
REG_FIELD(&xpd->requested_reply, regnum) == REG_FIELD(info, regnum) &&
REG_FIELD(&xpd->requested_reply, do_subreg) == REG_FIELD(info, do_subreg) &&
REG_FIELD(&xpd->requested_reply, subreg) == REG_FIELD(info, subreg)) {
xpd->last_reply = *info;
}
return 0;
}
static xproto_table_t PROTO_TABLE(FXO) = {
.owner = THIS_MODULE,
.entries = {
/* Prototable Card Opcode */
XENTRY( FXO, FXO, SIG_CHANGED ),
},
.name = "FXO", /* protocol name */
.ports_per_subunit = 8,
.type = XPD_TYPE_FXO,
.xops = {
.card_new = FXO_card_new,
.card_init = FXO_card_init,
.card_remove = FXO_card_remove,
.card_dahdi_preregistration = FXO_card_dahdi_preregistration,
.card_dahdi_postregistration = FXO_card_dahdi_postregistration,
.card_hooksig = FXO_card_hooksig,
.card_tick = FXO_card_tick,
.card_pcm_recompute = generic_card_pcm_recompute,
.card_pcm_fromspan = generic_card_pcm_fromspan,
.card_pcm_tospan = generic_card_pcm_tospan,
.card_timing_priority = generic_timing_priority,
.card_ioctl = FXO_card_ioctl,
.card_open = FXO_card_open,
.card_register_reply = FXO_card_register_reply,
.XPD_STATE = XPROTO_CALLER(FXO, XPD_STATE),
},
.packet_is_valid = fxo_packet_is_valid,
.packet_dump = fxo_packet_dump,
};
static bool fxo_packet_is_valid(xpacket_t *pack)
{
const xproto_entry_t *xe;
//DBG(GENERAL, "\n");
xe = xproto_card_entry(&PROTO_TABLE(FXO), XPACKET_OP(pack));
return xe != NULL;
}
static void fxo_packet_dump(const char *msg, xpacket_t *pack)
{
DBG(GENERAL, "%s\n", msg);
}
/*------------------------- DAA Handling --------------------------*/
#ifdef CONFIG_PROC_FS
static int proc_fxo_info_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
int len = 0;
unsigned long flags;
xpd_t *xpd = data;
struct FXO_priv_data *priv;
int i;
if(!xpd)
return -ENODEV;
spin_lock_irqsave(&xpd->lock, flags);
priv = xpd->priv;
BUG_ON(!priv);
len += sprintf(page + len, "\t%-17s: ", "Channel");
for_each_line(xpd, i) {
if(!IS_SET(xpd->digital_outputs, i) && !IS_SET(xpd->digital_inputs, i))
len += sprintf(page + len, "%4d ", i % 10);
}
len += sprintf(page + len, "\nLeds:");
len += sprintf(page + len, "\n\t%-17s: ", "state");
for_each_line(xpd, i) {
if(!IS_SET(xpd->digital_outputs, i) && !IS_SET(xpd->digital_inputs, i))
len += sprintf(page + len, " %d%d ",
IS_SET(priv->ledstate[LED_GREEN], i),
IS_SET(priv->ledstate[LED_RED], i));
}
len += sprintf(page + len, "\n\t%-17s: ", "blinking");
for_each_line(xpd, i) {
if(!IS_SET(xpd->digital_outputs, i) && !IS_SET(xpd->digital_inputs, i))
len += sprintf(page + len, " %d%d ",
IS_BLINKING(priv,i,LED_GREEN),
IS_BLINKING(priv,i,LED_RED));
}
len += sprintf(page + len, "\nBattery-Data:");
len += sprintf(page + len, "\n\t%-17s: ", "voltage");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->battery_voltage[i]);
}
len += sprintf(page + len, "\n\t%-17s: ", "current");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->battery_current[i]);
}
len += sprintf(page + len, "\nBattery:");
len += sprintf(page + len, "\n\t%-17s: ", "on");
for_each_line(xpd, i) {
char *bat;
if(priv->battery[i] == BATTERY_ON)
bat = "+";
else if(priv->battery[i] == BATTERY_OFF)
bat = "-";
else
bat = ".";
len += sprintf(page + len, "%4s ", bat);
}
len += sprintf(page + len, "\n\t%-17s: ", "debounce");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->nobattery_debounce[i]);
}
len += sprintf(page + len, "\nPolarity-Reverse:");
len += sprintf(page + len, "\n\t%-17s: ", "polarity");
for_each_line(xpd, i) {
char *polname;
if(priv->polarity[i] == POL_POSITIVE)
polname = "+";
else if(priv->polarity[i] == POL_NEGATIVE)
polname = "-";
else
polname = ".";
len += sprintf(page + len, "%4s ", polname);
}
len += sprintf(page + len, "\n\t%-17s: ", "debounce");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->polarity_debounce[i]);
}
len += sprintf(page + len, "\nPower-Denial:");
len += sprintf(page + len, "\n\t%-17s: ", "power");
for_each_line(xpd, i) {
char *curr;
if(priv->power[i] == POWER_ON)
curr = "+";
else if(priv->power[i] == POWER_OFF)
curr = "-";
else
curr = ".";
len += sprintf(page + len, "%4s ", curr);
}
len += sprintf(page + len, "\n\t%-17s: ", "safezone");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->power_denial_safezone[i]);
}
len += sprintf(page + len, "\n\t%-17s: ", "delay");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->power_denial_delay[i]);
}
#ifdef WITH_METERING
len += sprintf(page + len, "\nMetering:");
len += sprintf(page + len, "\n\t%-17s: ", "count");
for_each_line(xpd, i) {
len += sprintf(page + len, "%4d ", priv->metering_count[i]);
}
#endif
len += sprintf(page + len, "\n");
spin_unlock_irqrestore(&xpd->lock, flags);
if (len <= off+count)
*eof = 1;
*start = page + off;
len -= off;
if (len > count)
len = count;
if (len < 0)
len = 0;
return len;
}
#endif
#ifdef WITH_METERING
static int proc_xpd_metering_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
int len = 0;
unsigned long flags;
xpd_t *xpd = data;
struct FXO_priv_data *priv;
int i;
if(!xpd)
return -ENODEV;
priv = xpd->priv;
BUG_ON(!priv);
spin_lock_irqsave(&xpd->lock, flags);
len += sprintf(page + len, "# Chan\tMeter (since last read)\n");
for_each_line(xpd, i) {
len += sprintf(page + len, "%d\t%d\n",
i, priv->metering_count[i]);
}
spin_unlock_irqrestore(&xpd->lock, flags);
if (len <= off+count)
*eof = 1;
*start = page + off;
len -= off;
if (len > count)
len = count;
if (len < 0)
len = 0;
/* Zero meters */
for_each_line(xpd, i)
priv->metering_count[i] = 0;
return len;
}
#endif
static DEVICE_ATTR_READER(fxo_battery_show, dev, buf)
{
xpd_t *xpd;
struct FXO_priv_data *priv;
unsigned long flags;
int len = 0;
int i;
BUG_ON(!dev);
xpd = dev_to_xpd(dev);
if(!xpd)
return -ENODEV;
priv = xpd->priv;
BUG_ON(!priv);
spin_lock_irqsave(&xpd->lock, flags);
for_each_line(xpd, i) {
char bat;
if(priv->battery[i] == BATTERY_ON)
bat = '+';
else if(priv->battery[i] == BATTERY_OFF)
bat = '-';
else
bat = '.';
len += sprintf(buf + len, "%c ", bat);
}
len += sprintf(buf + len, "\n");
spin_unlock_irqrestore(&xpd->lock, flags);
return len;
}
static DEVICE_ATTR(fxo_battery, S_IRUGO, fxo_battery_show, NULL);
static int fxo_xpd_probe(struct device *dev)
{
xpd_t *xpd;
int ret;
xpd = dev_to_xpd(dev);
/* Is it our device? */
if(xpd->type != XPD_TYPE_FXO) {
XPD_ERR(xpd, "drop suggestion for %s (%d)\n",
dev_name(dev), xpd->type);
return -EINVAL;
}
XPD_DBG(DEVICES, xpd, "SYSFS\n");
ret = device_create_file(dev, &dev_attr_fxo_battery);
if(ret) {
XPD_ERR(xpd, "%s: device_create_file(fxo_battery) failed: %d\n", __FUNCTION__, ret);
goto fail_fxo_battery;
}
return 0;
fail_fxo_battery:
return ret;
}
static int fxo_xpd_remove(struct device *dev)
{
xpd_t *xpd;
xpd = dev_to_xpd(dev);
XPD_DBG(DEVICES, xpd, "SYSFS\n");
device_remove_file(dev, &dev_attr_fxo_battery);
return 0;
}
static struct xpd_driver fxo_driver = {
.type = XPD_TYPE_FXO,
.driver = {
.name = "fxo",
#ifndef OLD_HOTPLUG_SUPPORT
.owner = THIS_MODULE,
#endif
.probe = fxo_xpd_probe,
.remove = fxo_xpd_remove
}
};
static int __init card_fxo_startup(void)
{
int ret;
if(ring_debounce <= 0) {
ERR("ring_debounce=%d. Must be positive number of ticks\n", ring_debounce);
return -EINVAL;
}
if((ret = xpd_driver_register(&fxo_driver.driver)) < 0)
return ret;
INFO("revision %s\n", XPP_VERSION);
#ifdef WITH_METERING
INFO("FEATURE: WITH METERING Detection\n");
#else
INFO("FEATURE: NO METERING Detection\n");
#endif
xproto_register(&PROTO_TABLE(FXO));
return 0;
}
static void __exit card_fxo_cleanup(void)
{
xproto_unregister(&PROTO_TABLE(FXO));
xpd_driver_unregister(&fxo_driver.driver);
}
MODULE_DESCRIPTION("XPP FXO Card Driver");
MODULE_AUTHOR("Oron Peled <oron@actcom.co.il>");
MODULE_LICENSE("GPL");
MODULE_VERSION(XPP_VERSION);
MODULE_ALIAS_XPD(XPD_TYPE_FXO);
module_init(card_fxo_startup);
module_exit(card_fxo_cleanup);