summaryrefslogtreecommitdiff
path: root/xpp/card_fxs.c
diff options
context:
space:
mode:
authortzafrir <tzafrir@5390a7c7-147a-4af0-8ec9-7488f05a26cb>2007-02-28 01:23:19 +0000
committertzafrir <tzafrir@5390a7c7-147a-4af0-8ec9-7488f05a26cb>2007-02-28 01:23:19 +0000
commitff8823199f375d709a689dd017950d575b649df6 (patch)
tree7244e80bb45da5f27b747c8bc9aee36b984dd5cd /xpp/card_fxs.c
parent121cb4b570046fe612938d1eb401181c970b4636 (diff)
Merge xpp rev. 3495:
------------------------------------------------------------------------ r2243 | tzafrir | 2007-02-28 02:05:59 +0200 (Wed, 28 Feb 2007) | 4 lines * xpp rev. 3495: fix a race in the FXO driver of recent weeks. * Add the Astribank BRI driver (though still needs bristuffed zaptel to build and thus will not build by default) ------------------------------------------------------------------------ r2239 | tzafrir | 2007-02-27 08:14:18 +0200 (Tue, 27 Feb 2007) | 18 lines Xorcom rev. 3491: * Version of xpp modules is set from xpp/.version, rather than "unknown". * Astribank devices are now initialized in parallel: faster startup when there are multiple Astribanks. * Re-added support for the old format of /proc/xpp/sync write: (echo N 0 > /proc/xpp/sync ) . The new format (SYNC=NN) is preffered. * Firmware update to fix a PCM issue. * Fixed a build issue with kernel 2.6.8 . * Fixed missing initialization in Zaptel::Xpp::Xbus . * genzaptelconf will now set FXS ports as LS by default. To set them as KS, use fxs_default_start=ks in /etc/default/zaptel / /etc/sysconfig/zaptel (Also a workaround for #7755 ). * Groundwork for sync from zaptel master span: if zaptel is built with ZAPTEL_SYNC_TIC (see zaptel/team/tzafrir/sync ), xpp will report its drift from the zaptel sync master. * USB firmware update: had bad lines checksums (and fxload did not report). * fpga_load can now better report bad hex file checksum ;-) . ------------------------------------------------------------------------ r2223 | tzafrir | 2007-02-24 03:05:05 +0200 (Sat, 24 Feb 2007) | 3 lines Add the Zaptel and Zaptel::Xpp perl modules, and some simple utilities that use them. disabled by default for now. ------------------------------------------------------------------------ r2222 | tzafrir | 2007-02-24 02:55:05 +0200 (Sat, 24 Feb 2007) | 2 lines Make the xpp/utils/Makefile in 1.2 closer to the one in 1.4 . git-svn-id: http://svn.digium.com/svn/zaptel/trunk@2247 5390a7c7-147a-4af0-8ec9-7488f05a26cb
Diffstat (limited to 'xpp/card_fxs.c')
-rw-r--r--xpp/card_fxs.c286
1 files changed, 220 insertions, 66 deletions
diff --git a/xpp/card_fxs.c b/xpp/card_fxs.c
index af12cd2..fd50c3d 100644
--- a/xpp/card_fxs.c
+++ b/xpp/card_fxs.c
@@ -34,7 +34,8 @@
static const char rcsid[] = "$Id$";
DEF_PARM(int, print_dbg, 0, 0600, "Print DBG statements"); /* must be before zap_debug.h */
-DEF_PARM_BOOL(poll_digital_inputs, 1, 0600, "Poll Digital Inputs"); /* must be before zap_debug.h */
+DEF_PARM_BOOL(poll_digital_inputs, 1, 0600, "Poll Digital Inputs");
+DEF_PARM_BOOL(reversepolarity, 0, 0600, "Reverse Line Polarity");
/* Signaling is opposite (fxo signalling for fxs card) */
#if 1
@@ -69,6 +70,22 @@ static /* 0x0F */ DECLARE_CMD(FXS, REGISTER_REQUEST, byte chipsel, bool writing,
#define VALID_CHIPSEL(x) (((chipsel) >= 0 && (chipsel) <= 7) || (chipsel) == ALL_CHANS)
+/* Values of SLIC linefeed control register (0x40) */
+enum fxs_state {
+ FXS_LINE_OPEN = 0x00, /* Open */
+ FXS_LINE_ACTIVE = 0x01, /* Forward active */
+ FXS_LINE_OHTRANS = 0x02, /* Forward on-hook transmission */
+ FXS_LINE_TIPOPEN = 0x03, /* TIP open */
+ FXS_LINE_RING = 0x04, /* Ringing */
+ FXS_LINE_REV_ACTIVE = 0x05, /* Reverse active */
+ FXS_LINE_REV_OHTRANS = 0x06, /* Reverse on-hook transmission */
+ FXS_LINE_RING_OPEN = 0x07 /* RING open */
+};
+
+#define FXS_LINE_POL_ACTIVE ((reversepolarity) ? FXS_LINE_REV_ACTIVE : FXS_LINE_ACTIVE)
+#define FXS_LINE_POL_OHTRANS ((reversepolarity) ? FXS_LINE_REV_OHTRANS : FXS_LINE_OHTRANS)
+
+
/*---------------- FXS Protocol Commands ----------------------------------*/
static /* 0x0F */ DECLARE_CMD(FXS, XPD_STATE, bool on);
@@ -86,13 +103,18 @@ static void start_stop_vm_led(xbus_t *xbus, xpd_t *xpd, lineno_t pos);
#define PROC_FXS_INFO_FNAME "fxs_info"
struct FXS_priv_data {
- struct proc_dir_entry *regfile;
- struct proc_dir_entry *fxs_info;
- xpp_line_t ledstate[NUM_LEDS]; /* 0 - OFF, 1 - ON */
- xpp_line_t ledcontrol[NUM_LEDS]; /* 0 - OFF, 1 - ON */
- xpp_line_t found_fsk_pattern;
- xpp_line_t msg_waiting;
- int led_counter[NUM_LEDS][CHANNELS_PERXPD];
+ struct proc_dir_entry *regfile;
+ struct proc_dir_entry *fxs_info;
+ xpp_line_t ledstate[NUM_LEDS]; /* 0 - OFF, 1 - ON */
+ xpp_line_t ledcontrol[NUM_LEDS]; /* 0 - OFF, 1 - ON */
+ xpp_line_t found_fsk_pattern;
+ xpp_line_t msg_waiting;
+ xpp_line_t update_offhook_state;
+ int led_counter[NUM_LEDS][CHANNELS_PERXPD];
+ int ohttimer[CHANNELS_PERXPD];
+#define OHT_TIMER 6000 /* How long after RING to retain OHT */
+ enum fxs_state idletxhookstate[CHANNELS_PERXPD]; /* IDLE changing hook state */
+ enum fxs_state lasttxhook[CHANNELS_PERXPD];
};
/*
@@ -108,6 +130,16 @@ struct FXS_priv_data {
#define LED_BLINK_RING (1000/8) /* in ticks */
/*---------------- FXS: Static functions ----------------------------------*/
+static int linefeed_control(xbus_t *xbus, xpd_t *xpd, lineno_t chan, enum fxs_state value)
+{
+ struct FXS_priv_data *priv;
+
+ priv = xpd->priv;
+ DBG("%s/%s/%d: value=0x%02X\n", xbus->busname, xpd->xpdname, chan, value);
+ priv->lasttxhook[chan] = value;
+ return SLIC_DIRECT_REQUEST(xbus, xpd, chan, SLIC_WRITE, 0x40, value);
+}
+
static int do_chan_power(xbus_t *xbus, xpd_t *xpd, lineno_t chan, bool on)
{
int value = (on) ? 0x06 : 0x00;
@@ -223,21 +255,20 @@ static void handle_fxs_leds(xpd_t *xpd)
}
}
-static int do_callerid(xbus_t *xbus, xpd_t *xpd, lineno_t chan)
+static void restore_leds(xpd_t *xpd)
{
- int ret = 0;
- int i;
+ struct FXS_priv_data *priv;
+ int i;
- BUG_ON(!xbus);
- BUG_ON(!xpd);
- DBG("%s/%s/%d:\n", xbus->busname, xpd->xpdname, chan);
- ret = SLIC_DIRECT_REQUEST(xbus, xpd, chan, SLIC_WRITE, 0x40, FXS_LINE_CID);
- for_each_line(xpd, i)
- xpd->lasttxhook[i] = FXS_LINE_CID;
- return ret;
+ priv = xpd->priv;
+ for_each_line(xpd, i) {
+ if(IS_SET(xpd->offhook, i))
+ MARK_ON(priv, i, LED_GREEN);
+ else
+ MARK_OFF(priv, i, LED_GREEN);
+ }
}
-
/*---------------- FXS: Methods -------------------------------------------*/
static xpd_t *FXS_card_new(xbus_t *xbus, int xpd_num, const xproto_table_t *proto_table, byte revision)
@@ -310,6 +341,9 @@ static int FXS_card_init(xbus_t *xbus, xpd_t *xpd)
priv->regfile->read_proc = proc_xpd_register_read;
priv->regfile->data = xpd;
#endif
+ for_each_line(xpd, i) {
+ priv->idletxhookstate[i] = FXS_LINE_POL_ACTIVE;
+ }
ret = run_initialize_registers(xpd);
if(ret < 0)
goto err;
@@ -333,6 +367,7 @@ static int FXS_card_init(xbus_t *xbus, xpd_t *xpd)
do_led(xpd, i, LED_GREEN, 0);
msleep(50);
}
+ restore_leds(xpd);
return 0;
err:
clean_proc(xbus, xpd);
@@ -404,19 +439,26 @@ static int FXS_card_zaptel_postregistration(xpd_t *xpd, bool on)
MARK_OFF(priv, i, LED_RED);
msleep(2);
}
+ restore_leds(xpd);
return 0;
}
int FXS_card_hooksig(xbus_t *xbus, xpd_t *xpd, int pos, zt_txsig_t txsig)
{
- int ret = 0;
+ struct FXS_priv_data *priv;
+ int ret = 0;
+ struct zt_chan *chan = NULL;
+ enum fxs_state txhook;
DBG("%s/%s/%d: %s\n", xbus->busname, xpd->xpdname, pos, txsig2str(txsig));
+ priv = xpd->priv;
BUG_ON(xpd->direction != TO_PHONE);
if (IS_SET(xpd->digital_inputs, pos)) {
DBG("Ignoring signal sent to digital input line\n");
return 0;
}
+ if(SPAN_REGISTERED(xpd))
+ chan = &xpd->span.chans[pos];
switch(txsig) {
case ZT_TXSIG_ONHOOK:
xpd->ringing[pos] = 0;
@@ -426,41 +468,54 @@ int FXS_card_hooksig(xbus_t *xbus, xpd_t *xpd, int pos, zt_txsig_t txsig)
ret = CALL_XMETHOD(RELAY_OUT, xpd->xbus, xpd, pos-8, 0);
return ret;
}
+ if (priv->lasttxhook[pos] == FXS_LINE_OPEN) {
+ /*
+ * Restore state after KEWL hangup.
+ */
+ DBG("%s/%s/%d: KEWL STOP\n",
+ xbus->busname, xpd->xpdname, pos);
+ linefeed_control(xbus, xpd, pos, FXS_LINE_POL_ACTIVE);
+ if(IS_SET(xpd->offhook, pos))
+ MARK_ON(priv, pos, LED_GREEN);
+ }
ret = CALL_XMETHOD(RING, xbus, xpd, pos, 0); // RING off
if (!IS_SET(xpd->offhook, pos))
start_stop_vm_led(xbus, xpd, pos);
-#if 0
- switch(chan->sig) {
- case ZT_SIG_EM:
- case ZT_SIG_FXOKS:
- case ZT_SIG_FXOLS:
- xpd->lasttxhook[pos] = xpd->idletxhookstate[pos];
- break;
- case ZT_SIG_FXOGS:
- xpd->lasttxhook[pos] = FXS_LINE_TIPOPEN;
- break;
+ txhook = priv->lasttxhook[pos];
+ if(chan) {
+ switch(chan->sig) {
+ case ZT_SIG_EM:
+ case ZT_SIG_FXOKS:
+ case ZT_SIG_FXOLS:
+ txhook = priv->idletxhookstate[pos];
+ break;
+ case ZT_SIG_FXOGS:
+ txhook = FXS_LINE_TIPOPEN;
+ break;
+ }
}
-#endif
+ ret = linefeed_control(xbus, xpd, pos, txhook);
break;
case ZT_TXSIG_OFFHOOK:
+ txhook = priv->lasttxhook[pos];
if(xpd->ringing[pos]) {
BIT_SET(xpd->cid_on, pos);
- ret = do_callerid(xpd->xbus, xpd, pos); // CALLER ID
+ txhook = FXS_LINE_OHTRANS;
}
xpd->ringing[pos] = 0;
-#if 0
- switch(chan->sig) {
- case ZT_SIG_EM:
- xpd->lasttxhook[pos] = FXS_LINE_REV_ACTIVE;
- break;
- default:
- xpd->lasttxhook[pos] = xpd->idletxhookstate[pos];
- break;
+ if(chan) {
+ switch(chan->sig) {
+ case ZT_SIG_EM:
+ txhook = FXS_LINE_POL_ACTIVE;
+ break;
+ default:
+ txhook = priv->idletxhookstate[pos];
+ break;
+ }
}
-#endif
+ ret = linefeed_control(xbus, xpd, pos, txhook);
break;
case ZT_TXSIG_START:
- xpd->lasttxhook[pos] = FXS_LINE_RING;
xpd->ringing[pos] = 1;
BIT_CLR(xpd->cid_on, pos);
if(IS_SET(xpd->digital_outputs, pos)) {
@@ -471,7 +526,9 @@ int FXS_card_hooksig(xbus_t *xbus, xpd_t *xpd, int pos, zt_txsig_t txsig)
ret = CALL_XMETHOD(RING, xbus, xpd, pos, 1); // RING on
break;
case ZT_TXSIG_KEWL:
- xpd->lasttxhook[pos] = FXS_LINE_DISABLED;
+ DBG("%s/%s/%d: KEWL START\n", xbus->busname, xpd->xpdname, pos);
+ linefeed_control(xbus, xpd, pos, FXS_LINE_OPEN);
+ MARK_OFF(priv, pos, LED_GREEN);
break;
default:
NOTICE("%s: Can't set tx state to %s (%d)\n", __FUNCTION__, txsig2str(txsig), txsig);
@@ -553,34 +610,65 @@ static int set_vm_led_mode(xbus_t *xbus, xpd_t *xpd, int pos, int on)
static void start_stop_vm_led(xbus_t *xbus, xpd_t *xpd, lineno_t pos)
{
+ struct FXS_priv_data *priv;
bool on;
+ BUG_ON(!xpd);
if (IS_SET(xpd->digital_outputs | xpd->digital_inputs, pos))
return;
+ priv = xpd->priv;
on = IS_SET(((struct FXS_priv_data *)xpd->priv)->msg_waiting, pos);
DBG("%s/%s/%d %s\n", xbus->busname, xpd->xpdname, pos, (on)?"ON":"OFF");
set_vm_led_mode(xbus, xpd, pos, on);
do_chan_power(xbus, xpd, pos, on);
- SLIC_DIRECT_REQUEST(xbus, xpd, pos, SLIC_WRITE, 0x40, (on) ? 0x04 : 0x01);
+ linefeed_control(xbus, xpd, pos, (on) ? FXS_LINE_RING : priv->idletxhookstate[pos]);
}
static int FXS_chan_onhooktransfer(xbus_t *xbus, xpd_t *xpd, lineno_t chan, int millies)
{
- int ret = 0;
+ struct FXS_priv_data *priv;
+ int ret = 0;
BUG_ON(!xpd);
+ priv = xpd->priv;
BUG_ON(chan == ALL_CHANS);
DBG("%s/%s/%d: (%d millies)\n", xbus->busname, xpd->xpdname, chan, millies);
- xpd->ohttimer[chan] = millies << 3;
- xpd->idletxhookstate[chan] = FXS_LINE_CID; /* OHT mode when idle */
- if (xpd->lasttxhook[chan] == FXS_LINE_ENABLED) {
- /* Apply the change if appropriate */
- ret = do_callerid(xpd->xbus, xpd, chan); // CALLER ID
- }
- start_stop_vm_led(xbus, xpd, chan);
+ if(!IS_SET(xpd->offhook, chan))
+ start_stop_vm_led(xbus, xpd, chan);
return ret;
}
+static int FXS_card_open(xpd_t *xpd, lineno_t chan)
+{
+ struct FXS_priv_data *priv;
+ bool is_offhook;
+
+ BUG_ON(!xpd);
+ priv = xpd->priv;
+ is_offhook = IS_SET(xpd->offhook, chan);
+ DBG("%s/%s:%d (is %shook)\n", xpd->xbus->busname, xpd->xpdname,
+ chan, (is_offhook)?"off":"on");
+ /*
+ * Delegate updating zaptel to FXS_card_tick():
+ * The problem is that zt_hooksig() is spinlocking the channel and
+ * we are called by zaptel with the spinlock already held on the
+ * same channel.
+ */
+ BIT_SET(priv->update_offhook_state, chan);
+ return 0;
+}
+
+static int FXS_card_close(xpd_t *xpd, lineno_t chan)
+{
+ struct FXS_priv_data *priv;
+
+ BUG_ON(!xpd);
+ DBG("%s/%s:%d\n", xpd->xbus->busname, xpd->xpdname, chan);
+ priv = xpd->priv;
+ priv->idletxhookstate[chan] = FXS_LINE_POL_ACTIVE;
+ return 0;
+}
+
/*
* INPUT polling is done via SLIC register 0x06 (same as LEDS):
* 7 6 5 4 3 2 1 0
@@ -603,6 +691,36 @@ static void poll_inputs(xbus_t *xbus, xpd_t *xpd)
}
}
+void handle_linefeed(xpd_t *xpd)
+{
+ struct FXS_priv_data *priv;
+ int i;
+
+ BUG_ON(!xpd);
+ priv = xpd->priv;
+ BUG_ON(!priv);
+ for_each_line(xpd, i) {
+ if (priv->lasttxhook[i] == FXS_LINE_RING) {
+ /* RINGing, prepare for OHT */
+ priv->ohttimer[i] = OHT_TIMER;
+ priv->idletxhookstate[i] = FXS_LINE_POL_OHTRANS;
+ } else {
+ if (priv->ohttimer[i]) {
+ priv->ohttimer[i]--;
+ if (!priv->ohttimer[i]) {
+ priv->idletxhookstate[i] = FXS_LINE_POL_ACTIVE;
+ if (priv->lasttxhook[i] == FXS_LINE_POL_OHTRANS) {
+ enum fxs_state txhook = FXS_LINE_POL_ACTIVE;
+ /* Apply the change if appropriate */
+ BIT_CLR(xpd->cid_on, i);
+ linefeed_control(xpd->xbus, xpd, i, txhook);
+ }
+ }
+ }
+ }
+ }
+}
+
#ifndef VMWI_IOCTL
/*
* Detect Voice Mail Waiting Indication
@@ -678,6 +796,28 @@ static int FXS_card_tick(xbus_t *xbus, xpd_t *xpd)
}
#endif
handle_fxs_leds(xpd);
+ handle_linefeed(xpd);
+ if(priv->update_offhook_state) { /* set in FXS_card_open() */
+ int i;
+
+ for_each_line(xpd, i) {
+ if(!IS_SET(priv->update_offhook_state, i))
+ continue;
+ /*
+ * Update LEDs and zaptel with current state of line.
+ */
+ if(IS_SET(xpd->offhook, i)) {
+ NOTICE("%s/%s/%d: Already offhook during open. OK.\n",
+ xbus->busname, xpd->xpdname, i);
+ MARK_ON(priv, i, LED_GREEN);
+ update_line_status(xpd, i, 1);
+ } else {
+ MARK_OFF(priv, i, LED_GREEN);
+ update_line_status(xpd, i, 0);
+ }
+ BIT_CLR(priv->update_offhook_state, i);
+ }
+ }
#ifndef VMWI_IOCTL
if(SPAN_REGISTERED(xpd))
detect_vmwi(xpd);
@@ -721,9 +861,8 @@ static int FXS_card_tick(xbus_t *xbus, xpd_t *xpd)
static /* 0x0F */ HOSTCMD(FXS, XPD_STATE, bool on)
{
- int ret = 0;
int i;
- enum fxs_state value = (on) ? 0x01 : 0x00;
+ enum fxs_state value = (on) ? FXS_LINE_POL_ACTIVE : FXS_LINE_OPEN;
unsigned long flags;
struct FXS_priv_data *priv;
@@ -732,23 +871,22 @@ static /* 0x0F */ HOSTCMD(FXS, XPD_STATE, bool on)
priv = xpd->priv;
spin_lock_irqsave(&xpd->lock, flags);
DBG("%s/%s: %s\n", xbus->busname, xpd->xpdname, (on) ? "on" : "off");
- ret = SLIC_DIRECT_REQUEST(xbus, xpd, ALL_CHANS, SLIC_WRITE, 0x40, value);
for_each_line(xpd, i)
- xpd->lasttxhook[i] = value;
+ linefeed_control(xbus, xpd, i, value);
if(on) {
MARK_ON(priv, ALL_CHANS, LED_GREEN);
} else {
MARK_OFF(priv, ALL_CHANS, LED_GREEN);
}
spin_unlock_irqrestore(&xpd->lock, flags);
- return ret;
+ return 0;
}
static /* 0x0F */ HOSTCMD(FXS, RING, lineno_t chan, bool on)
{
int ret = 0;
struct FXS_priv_data *priv;
- enum fxs_state value = (on) ? 0x04 : 0x01;
+ enum fxs_state value = (on) ? FXS_LINE_RING : FXS_LINE_POL_ACTIVE;
BUG_ON(!xbus);
BUG_ON(!xpd);
@@ -756,8 +894,7 @@ static /* 0x0F */ HOSTCMD(FXS, RING, lineno_t chan, bool on)
priv = xpd->priv;
set_vm_led_mode(xbus, xpd, chan, 0);
do_chan_power(xbus, xpd, chan, on); // Power up (for ring)
- ret = SLIC_DIRECT_REQUEST(xbus, xpd, chan, SLIC_WRITE, 0x40, value);
- xpd->lasttxhook[chan] = value;
+ ret = linefeed_control(xbus, xpd, chan, value);
if(on) {
MARK_BLINK(priv, chan, LED_GREEN, LED_BLINK_RING);
} else {
@@ -819,7 +956,6 @@ HANDLER_DEF(FXS, SIG_CHANGED)
DBG("%s/%s/%d: ONHOOK\n", xbus->busname, xpd->xpdname, i);
MARK_OFF(priv, i, LED_GREEN);
update_line_status(xpd, i, 0);
- start_stop_vm_led(xbus, xpd, i);
}
}
}
@@ -910,6 +1046,8 @@ xproto_table_t PROTO_TABLE(FXS) = {
.card_hooksig = FXS_card_hooksig,
.card_tick = FXS_card_tick,
.chan_onhooktransfer = FXS_chan_onhooktransfer,
+ .card_open = FXS_card_open,
+ .card_close = FXS_card_close,
#ifdef VMWI_IOCTL
.card_ioctl = FXS_card_ioctl,
#endif
@@ -917,8 +1055,6 @@ xproto_table_t PROTO_TABLE(FXS) = {
.RING = XPROTO_CALLER(FXS, RING),
.RELAY_OUT = XPROTO_CALLER(FXS, RELAY_OUT),
.XPD_STATE = XPROTO_CALLER(FXS, XPD_STATE),
-
- .SYNC_SOURCE = XPROTO_CALLER(GLOBAL, SYNC_SOURCE),
},
.packet_is_valid = fxs_packet_is_valid,
.packet_dump = fxs_packet_dump,
@@ -954,10 +1090,28 @@ static int proc_fxs_info_read(char *page, char **start, off_t off, int count, in
spin_lock_irqsave(&xpd->lock, flags);
priv = xpd->priv;
BUG_ON(!priv);
- len += sprintf(page + len, "\t%-17s: ", "Channel");
+ len += sprintf(page + len, "%-8s %-10s %-10s %-10s\n",
+ "Channel",
+ "idletxhookstate",
+ "lasttxhook",
+ "ohttimer"
+ );
for_each_line(xpd, i) {
- if(!IS_SET(xpd->digital_outputs, i) && !IS_SET(xpd->digital_inputs, i))
- len += sprintf(page + len, "%d ", i % 10);
+ char pref;
+
+ if(IS_SET(xpd->digital_outputs, i))
+ pref = 'O';
+ else if(IS_SET(xpd->digital_inputs, i))
+ pref = 'I';
+ else
+ pref = ' ';
+ len += sprintf(page + len, "%c%7d %10d %10d %10d\n",
+ pref,
+ i,
+ priv->idletxhookstate[i],
+ priv->lasttxhook[i],
+ priv->ohttimer[i]
+ );
}
len += sprintf(page + len, "\n");
for(led = 0; led < NUM_LEDS; led++) {