dahdi-linux/drivers/dahdi/xpp/card_fxo.c

1430 lines
39 KiB
C
Raw Normal View History

/*
* 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 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(PHONEDEV(xpd).digital_outputs, chan) || IS_SET(PHONEDEV(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(PHONEDEV(xpd).digital_outputs, i) || IS_SET(PHONEDEV(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 && !PHONEDEV(xpd).ringing[pos]) {
LINE_DBG(SIGNAL, xpd, pos, "START\n");
PHONEDEV(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 && PHONEDEV(xpd).ringing[pos]) {
LINE_DBG(SIGNAL, xpd, pos, "STOP\n");
PHONEDEV(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(PHONEDEV(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;
PHONEDEV(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_PHONE_METHOD(card_pcm_recompute, 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");
PHONEDEV(xpd).span.spantype = "FXO";
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(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(PHONEDEV(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(PHONEDEV(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) || PHONEDEV(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 -------------------------------------*/
/*---------------- 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(PHONEDEV(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(PHONEDEV(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 int FXO_card_state(xpd_t *xpd, bool on)
{
int ret = 0;
struct FXO_priv_data *priv;
BUG_ON(!xpd);
priv = xpd->priv;
BUG_ON(!priv);
XPD_DBG(GENERAL, xpd, "%s\n", (on) ? "on" : "off");
return ret;
}
static const struct xops fxo_xops = {
.card_new = FXO_card_new,
.card_init = FXO_card_init,
.card_remove = FXO_card_remove,
.card_tick = FXO_card_tick,
.card_register_reply = FXO_card_register_reply,
};
static const struct phoneops fxo_phoneops = {
.card_dahdi_preregistration = FXO_card_dahdi_preregistration,
.card_dahdi_postregistration = FXO_card_dahdi_postregistration,
.card_hooksig = FXO_card_hooksig,
.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_state = FXO_card_state,
};
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 = &fxo_xops,
.phoneops = &fxo_phoneops,
.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(PHONEDEV(xpd).digital_outputs, i) && !IS_SET(PHONEDEV(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(PHONEDEV(xpd).digital_outputs, i) && !IS_SET(PHONEDEV(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(PHONEDEV(xpd).digital_outputs, i) && !IS_SET(PHONEDEV(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);