dl-fldigi/src/cw_rtty/rtty.cxx

935 wiersze
19 KiB
C++

// ----------------------------------------------------------------------------
// rtty.cxx -- RTTY modem
//
// Copyright (C) 2006-2010
// Dave Freese, W1HKJ
//
// This file is part of fldigi. Adapted from code contained in gmfsk source code
// distribution.
// gmfsk Copyright (C) 2001, 2002, 2003
// Tomi Manninen (oh2bns@sral.fi)
//
// Fldigi 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 3 of the License, or
// (at your option) any later version.
//
// Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#include <config.h>
#include <iostream>
using namespace std;
#include "view_rtty.h"
#include "fl_digi.h"
#include "digiscope.h"
#include "misc.h"
#include "waterfall.h"
#include "confdialog.h"
#include "configuration.h"
#include "status.h"
#include "digiscope.h"
#include "trx.h"
#include "dl_fldigi/hbtint.h"
view_rtty *rttyviewer = (view_rtty *)0;
//=====================================================================
// Baudot support
//=====================================================================
static unsigned char letters[32] = {
'\0', 'E', '\n', 'A', ' ', 'S', 'I', 'U',
'\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K',
'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q',
'O', 'B', 'G', '·', 'M', 'X', 'V', '·'
};
#if 0
/*
* ITA-2 version of the figures case.
*/
static unsigned char figures[32] = {
'\0', '3', '\n', '-', ' ', '\'', '8', '7',
'\r', '·', '4', '\a', ',', '·', ':', '(',
'5', '+', ')', '2', '·', '6', '0', '1',
'9', '?', '·', '·', '.', '/', '=', '·'
};
#endif
#if 1
/*
* U.S. version of the figures case.
*/
static unsigned char figures[32] = {
'\0', '3', '\n', '-', ' ', '\a', '8', '7',
'\r', '$', '4', '\'', ',', '!', ':', '(',
'5', '"', ')', '2', '#', '6', '0', '1',
'9', '?', '&', '·', '.', '/', ';', '·'
};
#endif
#if 0
/*
* A mix of the two. This is what seems to be what people actually use.
*/
static unsigned char figures[32] = {
'\0', '3', '\n', '-', ' ', '\'', '8', '7',
'\r', '$', '4', '\a', ',', '!', ':', '(',
'5', '+', ')', '2', '#', '6', '0', '1',
'9', '?', '&', '·', '.', '/', '=', '·'
};
#endif
int dspcnt = 0;
static char msg1[20];
/* Terminating 0 at the end of the list for dl_fldigi/flights.cxx */
const double rtty::SHIFT[] = {23, 85, 160, 170, 182, 200, 240, 350, 425, 600, 850, 0};
const double rtty::BAUD[] = {45, 45.45, 50, 56, 75, 100, 110, 150, 200, 300, 600, 1200, 0};
const int rtty::BITS[] = {5, 7, 8};
void rtty::tx_init(SoundBase *sc)
{
// start each new transmission 20 bit lengths MARK tone
scard = sc;
phaseacc = 0;
preamble = 20;
videoText();
}
void rtty::rx_init()
{
rxstate = RTTY_RX_STATE_IDLE;
rxmode = LETTERS;
phaseacc = 0;
FSKphaseacc = 0;
for (int i = 0; i < RTTYMaxSymLen; i++ ) {
bbfilter[i] = 0.0;
}
bitfilt->reset();
poserr = negerr = 0.0;
lost = 0;
}
void rtty::init()
{
bool wfrev = wf->Reverse();
bool wfsb = wf->USB();
reverse = wfrev ^ !wfsb;
stopflag = false;
if (progdefaults.StartAtSweetSpot)
set_freq(progdefaults.RTTYsweetspot);
else if (progStatus.carrier != 0) {
set_freq(progStatus.carrier);
#if !BENCHMARK_MODE
progStatus.carrier = 0;
#endif
} else
set_freq(wf->Carrier());
rx_init();
put_MODEstatus(mode);
if ((rtty_baud - (int)rtty_baud) == 0)
snprintf(msg1, sizeof(msg1), "%-3.0f / %-4.0f", rtty_baud, rtty_shift);
else
snprintf(msg1, sizeof(msg1), "%-4.2f / %-4.0f", rtty_baud, rtty_shift);
put_Status1(msg1);
if (progdefaults.PreferXhairScope)
set_scope_mode(Digiscope::XHAIRS);
else
set_scope_mode(Digiscope::RTTY);
}
rtty::~rtty()
{
if (hilbert) delete hilbert;
if (bitfilt) delete bitfilt;
if (bpfilt) delete bpfilt;
if (pipe) delete [] pipe;
if (dsppipe) delete [] dsppipe;
}
void rtty::restart()
{
double stl;
rtty_shift = shift = (progdefaults.rtty_shift >= 0 ?
SHIFT[progdefaults.rtty_shift] : progdefaults.rtty_custom_shift);
rtty_baud = BAUD[progdefaults.rtty_baud];
nbits = rtty_bits = BITS[progdefaults.rtty_bits];
if (rtty_bits == 5)
rtty_parity = RTTY_PARITY_NONE;
else
switch (progdefaults.rtty_parity) {
case 0 : rtty_parity = RTTY_PARITY_NONE; break;
case 1 : rtty_parity = RTTY_PARITY_EVEN; break;
case 2 : rtty_parity = RTTY_PARITY_ODD; break;
case 3 : rtty_parity = RTTY_PARITY_ZERO; break;
case 4 : rtty_parity = RTTY_PARITY_ONE; break;
default : rtty_parity = RTTY_PARITY_NONE; break;
}
rtty_stop = progdefaults.rtty_stop;
txmode = LETTERS;
rxmode = LETTERS;
symbollen = (int) (samplerate / rtty_baud + 0.5);
set_bandwidth(shift);
if (progdefaults.RTTY_BW < rtty_baud)
progdefaults.RTTY_BW = rtty_baud;
rtty_BW = progdefaults.RTTY_BW;
sldrRTTYbandwidth->value(rtty_BW);
wf->redraw_marker();
bp_filt_lo = (shift/2.0 - rtty_BW/2.0) / samplerate;
if (bp_filt_lo < 0) bp_filt_lo = 0;
bp_filt_hi = (shift/2.0 + rtty_BW/2.0) / samplerate;
if (bpfilt)
bpfilt->create_filter(bp_filt_lo, bp_filt_hi);
else
bpfilt = new fftfilt(bp_filt_lo, bp_filt_hi, 1024);
bflen = symbollen/3;
if (bitfilt)
bitfilt->setLength(bflen);
else
bitfilt = new Cmovavg(bflen);
// stop length = 1, 1.5 or 2 bits
rtty_stop = progdefaults.rtty_stop;
if (rtty_stop == 0) stl = 1.0;
else if (rtty_stop == 1) stl = 1.5;
else stl = 2.0;
stoplen = (int) (stl * samplerate / rtty_baud + 0.5);
freqerr = 0.0;
filterptr = 0;
pipeptr = 0;
poscnt = negcnt = 0;
posfreq = negfreq = 0.0;
/* The length of a byte (including start, stop and parity bits */
bytelen = (1 + nbits + stl + (rtty_parity == RTTY_PARITY_NONE ? 0 : 1)) * symbollen;
metric = 0.0;
if ((rtty_baud - (int)rtty_baud) == 0)
snprintf(msg1, sizeof(msg1), "%-3.0f / %-4.0f", rtty_baud, rtty_shift);
else
snprintf(msg1, sizeof(msg1), "%-4.2f / %-4.0f", rtty_baud, rtty_shift);
put_Status1(msg1);
put_MODEstatus(mode);
for (int i = 0; i < 1024; i++) QI[i].re = QI[i].im = 0.0;
sigpwr = 0.0;
noisepwr = 0.0;
freqerrlo = freqerrhi = 0.0;
sigsearch = 0;
dspcnt = 2*(nbits + 2);
clear_zdata = true;
rttyviewer->restart();
}
rtty::rtty(trx_mode tty_mode)
{
cap |= CAP_AFC | CAP_REV;
mode = tty_mode;
samplerate = RTTY_SampleRate;
bpfilt = (fftfilt *)0;
bitfilt = (Cmovavg *)0;
hilbert = new C_FIR_filter();
hilbert->init_hilbert(37, 1);
pipe = new double[MAXPIPE];
dsppipe = new double [MAXPIPE];
samples = new complex[8];
::rttyviewer = new view_rtty(mode);
restart();
}
void rtty::update_syncscope()
{
int j;
for (int i = 0; i < symbollen; i++) {
j = pipeptr - i;
if (j < 0) j += symbollen;
dsppipe[i] = pipe[j];
}
set_scope(dsppipe, symbollen, false);
}
void rtty::clear_syncscope()
{
set_scope(0, 0, false);
}
complex rtty::mixer(complex in)
{
complex z;
z.re = cos(phaseacc);
z.im = sin(phaseacc);
z = z * in;
phaseacc -= TWOPI * frequency / samplerate;
if (phaseacc > M_PI)
phaseacc -= TWOPI;
else if (phaseacc < M_PI)
phaseacc += TWOPI;
return z;
}
unsigned char rtty::bitreverse(unsigned char in, int n)
{
unsigned char out = 0;
for (int i = 0; i < n; i++)
out = (out << 1) | ((in >> i) & 1);
return out;
}
static int rparity(int c)
{
int w = c;
int p = 0;
while (w) {
p += (w & 1);
w >>= 1;
}
return p & 1;
}
int rtty::rttyparity(unsigned int c)
{
c &= (1 << nbits) - 1;
switch (rtty_parity) {
default:
case RTTY_PARITY_NONE:
return 0;
case RTTY_PARITY_ODD:
return rparity(c);
case RTTY_PARITY_EVEN:
return !rparity(c);
case RTTY_PARITY_ZERO:
return 0;
case RTTY_PARITY_ONE:
return 1;
}
}
int rtty::decode_char()
{
unsigned int parbit, par, data;
parbit = (rxdata >> nbits) & 1;
par = rttyparity(rxdata);
if (rtty_parity != RTTY_PARITY_NONE && parbit != par)
return 0;
data = rxdata & ((1 << nbits) - 1);
if (nbits == 5)
return baudot_dec(data);
return data;
}
bool rtty::rx(bool bit)
{
bool flag = false;
unsigned char c;
int lb;
lost++;
switch (rxstate) {
case RTTY_RX_STATE_IDLE:
if (!bit) {
rxstate = RTTY_RX_STATE_START;
counter = symbollen / 2;
}
break;
case RTTY_RX_STATE_START:
if (--counter == 0) {
if (!bit) {
rxstate = RTTY_RX_STATE_DATA;
counter = symbollen;
bitcntr = 0;
rxdata = 0;
} else {
rxstate = RTTY_RX_STATE_IDLE;
}
} else
if (bit) rxstate = RTTY_RX_STATE_IDLE;
break;
case RTTY_RX_STATE_DATA:
if (--counter == 0) {
rxdata |= bit << bitcntr++;
counter = symbollen;
}
if (bitcntr == nbits) {
if (rtty_parity == RTTY_PARITY_NONE) {
rxstate = RTTY_RX_STATE_STOP;
}
else {
rxstate = RTTY_RX_STATE_PARITY;
}
}
break;
case RTTY_RX_STATE_PARITY:
if (--counter == 0) {
rxstate = RTTY_RX_STATE_STOP;
rxdata |= bit << bitcntr++;
counter = symbollen;
}
break;
case RTTY_RX_STATE_STOP:
if (--counter == 0) {
if (bit) {
if ((metric >= progStatus.sldrSquelchValue && progStatus.sqlonoff)|| !progStatus.sqlonoff) {
c = decode_char();
/* lb = estimated bytes lost */
lb = (lost - bytelen / 2) / bytelen;
/* HOOKS */
put_rx_ssdv(c, lb);
if (lb != 0)
dl_fldigi::hbtint::extrmgr->skipped(lb);
if (nbits == 5)
dl_fldigi::hbtint::extrmgr->push(c, habitat::PUSH_BAUDOT_HACK);
else
dl_fldigi::hbtint::extrmgr->push(c);
if ( c != 0 )
put_rx_char(progdefaults.rx_lowercase ? tolower(c) : c, FTextBase::RECV, true);
}
flag = true;
lost = 0;
}
rxstate = RTTY_RX_STATE_STOP2;
counter = symbollen / 2;
}
break;
case RTTY_RX_STATE_STOP2:
if (--counter == 0) {
rxstate = RTTY_RX_STATE_IDLE;
}
break;
}
return flag;
}
char snrmsg[80];
void rtty::Metric()
{
double delta = rtty_baud/2.0;
double np =
wf->powerDensity(frequency - shift * 1.5, delta) +
wf->powerDensity(frequency + shift * 1.5, delta) + 1e-10;
double sp =
wf->powerDensity(frequency - shift/2, delta) +
wf->powerDensity(frequency + shift/2, delta) + 1e-10;
double snr = 0;
sigpwr = decayavg( sigpwr, sp, sp - sigpwr > 0 ? 2 : 8);
noisepwr = decayavg( noisepwr, np, 32 );
snr = 10*log10(sigpwr / ( noisepwr * (1500 / delta))); // 3000 Hz noise bw
snprintf(snrmsg, sizeof(snrmsg), "s/n %3.0f dB", snr);
put_Status2(snrmsg);
metric = CLAMP(sigpwr/noisepwr, 0.0, 100.0);
display_metric(metric);
}
void rtty::searchDown()
{
double srchfreq = frequency - shift -100;
double minfreq = shift * 2 + 100;
double spwrlo, spwrhi, npwr;
while (srchfreq > minfreq) {
spwrlo = wf->powerDensity(srchfreq - shift/2, 2*rtty_baud);
spwrhi = wf->powerDensity(srchfreq + shift/2, 2*rtty_baud);
npwr = wf->powerDensity(srchfreq + shift, 2*rtty_baud) + 1e-10;
if ((spwrlo / npwr > 10.0) && (spwrhi / npwr > 10.0)) {
frequency = srchfreq;
set_freq(frequency);
sigsearch = SIGSEARCH;
break;
}
srchfreq -= 5.0;
}
}
void rtty::searchUp()
{
double srchfreq = frequency + shift +100;
double maxfreq = IMAGE_WIDTH - shift * 2 - 100;
double spwrhi, spwrlo, npwr;
while (srchfreq < maxfreq) {
spwrlo = wf->powerDensity(srchfreq - shift/2, 2*rtty_baud);
spwrhi = wf->powerDensity(srchfreq + shift/2, 2*rtty_baud);
npwr = wf->powerDensity(srchfreq - shift, 2*rtty_baud) + 1e-10;
if ((spwrlo / npwr > 10.0) && (spwrhi / npwr > 10.0)) {
frequency = srchfreq;
set_freq(frequency);
sigsearch = SIGSEARCH;
break;
}
srchfreq += 5.0;
}
}
int rtty::rx_process(const double *buf, int len)
{
complex z, *zp;
double f = 0.0;
double fin;
static bool bit = true;
int n = 0;
double deadzone = shift/4;
double rotate;
double ferr = 0;
if (progdefaults.RTTY_BW != rtty_BW) {
rtty_BW = progdefaults.RTTY_BW;
bp_filt_lo = (shift/2.0 - rtty_BW/2.0) / samplerate;
if (bp_filt_lo < 0) bp_filt_lo = 0;
bp_filt_hi = (shift/2.0 + rtty_BW/2.0) / samplerate;
bpfilt->create_filter(bp_filt_lo, bp_filt_hi);
wf->redraw_marker();
}
if (rttyviewer && !bHistory) rttyviewer->rx_process(buf, len);
Metric();
while (len-- > 0) {
// create analytic signal from sound card input samples
z.re = z.im = *buf++;
hilbert->run(z, z);
// mix it with the audio carrier frequency to create a baseband signal
z = mixer(z);
// bandpass filter using Windowed Sinc - Overlap-Add convolution filter
n = bpfilt->run(z, &zp);
if (n) {
for (int i = 0; i < n; i++) {
// measure phase difference between successive samples to determine
// the frequency of the baseband signal (+rtty_baud or -rtty_baud)
// see class complex definiton for operator %
fin = (prevsmpl % zp[i]).arg() * samplerate / TWOPI;
prevsmpl = zp[i];
// track the + and - frequency excursions separately to derive an afc signal
rotate = -4.0 * M_PI * freqerr / rtty_shift;
double xmix = fabs(4.0 * freqerr / rtty_shift);
if (xmix > 0.5) xmix = 0.5;
xmix += 0.05;
if (fin > 0.0) {
poscnt++;
posfreq += fin;
QI[i].re = zp[i].re;
QI[i].im = xmix * zp[i].im;
}
if (fin < 0.0) {
negcnt++;
negfreq += fin;
QI[i].re = xmix * zp[i].im;
QI[i].im = zp[i].re;
}
QI[i] = QI[i] * complex(cos(rotate), sin(rotate));
avgsig = decayavg(avgsig, 1.25 * zp[i].mag(), 128);//64);
if (avgsig > 0)
QI[i] = QI[i] / avgsig;
fin = CLAMP(fin, - rtty_shift, rtty_shift);
// filter the result with a moving average filter
f = bitfilt->run(fin);
// hysterisis dead zone in frequency discriminator bit detector
if (f > deadzone )
bit = true;
if (f < -deadzone)
bit = false;
if (dspcnt && (--dspcnt % (nbits + 2) == 0)) {
pipe[pipeptr] = f / shift;
pipeptr = (pipeptr + 1) % symbollen;
}
if ( rx( reverse ? !bit : bit ) ) {
dspcnt = symbollen * (nbits + 2);
if (poscnt && negcnt) {
poserr = posfreq/poscnt;
negerr = negfreq/negcnt;
// compute the frequency error as the median of + and - relative freq's
if (sigsearch) sigsearch--;
ferr = -(poserr + negerr)/(2*(SIGSEARCH - sigsearch + 1));
int fs = progdefaults.rtty_afcspeed;
int avging;
if (fs == 0) avging = 8;
else if (fs == 1) avging = 4;
else avging = 1;
freqerr = decayavg(freqerr, ferr, avging);
poscnt = 0; posfreq = 0.0;
negcnt = 0; negfreq = 0.0;
}
}
}
}
}
if (progStatus.afconoff) {
if (metric > progStatus.sldrSquelchValue || !progStatus.sqlonoff) // || sigsearch) {
set_freq(frequency - ferr);
}
if (metric < progStatus.sldrSquelchValue && progStatus.sqlonoff) {
if (clear_zdata) {
for (int i = 0; i < MAX_ZLEN; i++) QI[i].re = QI[i].im = 0.0;
set_zdata(QI, MAX_ZLEN);
clear_zdata = false;
clear_syncscope();
}
} else {
clear_zdata = true;
update_syncscope();
set_zdata(QI, n);
}
return 0;
}
//=====================================================================
// RTTY transmit
//=====================================================================
double rtty::nco(double freq)
{
phaseacc += TWOPI * freq / samplerate;
if (phaseacc > M_PI)
phaseacc -= TWOPI;
return cos(phaseacc);
}
double rtty::FSKnco()
{
FSKphaseacc += TWOPI * 1000 / samplerate;
if (FSKphaseacc > M_PI)
FSKphaseacc -= TWOPI;
return sin(FSKphaseacc);
}
void rtty::send_symbol(int symbol)
{
double freq;
if (reverse)
symbol = !symbol;
if (symbol)
freq = get_txfreq_woffset() + shift / 2.0;
else
freq = get_txfreq_woffset() - shift / 2.0;
for (int i = 0; i < symbollen; i++) {
outbuf[i] = nco(freq);
if (symbol)
FSKbuf[i] = FSKnco();
else
FSKbuf[i] = 0.0 * FSKnco();
}
if (progdefaults.PseudoFSK)
ModulateStereo(outbuf, FSKbuf, symbollen);
else
ModulateXmtr(outbuf, symbollen);
}
void rtty::send_stop()
{
double freq;
bool invert = reverse;
if (invert)
freq = get_txfreq_woffset() - shift / 2.0;
else
freq = get_txfreq_woffset() + shift / 2.0;
for (int i = 0; i < stoplen; i++) {
outbuf[i] = nco(freq);
if (invert)
FSKbuf[i] = 0.0 * FSKnco();
else
FSKbuf[i] = FSKnco();
}
if (progdefaults.PseudoFSK)
ModulateStereo(outbuf, FSKbuf, stoplen);
else
ModulateXmtr(outbuf, stoplen);
}
void rtty::send_char(int c)
{
int i;
if (nbits == 5) {
if (c == LETTERS)
c = 0x1F;
if (c == FIGURES)
c = 0x1B;
}
send_symbol(0);
// data bits
for (i = 0; i < nbits; i++) {
send_symbol((c >> i) & 1);
}
// parity bit
if (rtty_parity != RTTY_PARITY_NONE)
send_symbol(rttyparity(c));
// stop bit(s)
send_stop();
if (nbits == 5) {
if (c == 0x1F || c == 0x1B)
return;
if (txmode == LETTERS)
c = letters[c];
else
c = figures[c];
if (c)
put_echo_char(progdefaults.rx_lowercase ? tolower(c) : c);
}
}
void rtty::send_idle()
{
if (nbits == 5) {
send_char(LETTERS);
txmode = LETTERS;
} else
send_char(0);
}
static int line_char_count = 0;
int rtty::tx_process()
{
int c;
if (preamble > 0) {
for (int i = 0; i < preamble; i++)
send_symbol(1);
if (nbits == 5) {
send_char(LETTERS);
txmode = LETTERS;
}
preamble = 0;
}
c = get_tx_char();
// TX buffer empty
if (c == 3 || stopflag) {
stopflag = false;
line_char_count = 0;
if (nbits != 5) {
send_char('\r');
send_char('\r');
send_char('\n');
} else {
send_char(0x08);
send_char(0x08);
send_char(0x02);
}
// KeyLine->clearDTR();
cwid();
return -1;
}
// send idle character if c == -1
if (c == -1) {
send_idle();
return 0;
}
// if NOT Baudot
if (nbits != 5) {
send_char(c);
return 0;
}
if (isalpha(c) || isdigit(c) || isblank(c) || ispunct(c)) {
++line_char_count;
}
if (progdefaults.rtty_autocrlf && (c != '\n' && c != '\r') &&
(line_char_count == progdefaults.rtty_autocount ||
(line_char_count > progdefaults.rtty_autocount - 5 && c == ' '))) {
line_char_count = 0;
if (progdefaults.rtty_crcrlf)
send_char(0x08); // CR-CR-LF triplet
send_char(0x08);
send_char(0x02);
if (c == ' ')
return 0;
}
if (c == '\r') {
line_char_count = 0;
send_char(0x08);
return 0;
}
if (c == '\n') {
line_char_count = 0;
if (progdefaults.rtty_crcrlf)
send_char(0x08); // CR-CR-LF triplet
send_char(0x02);
return 0;
}
/* unshift-on-space */
if (c == ' ') {
if (progdefaults.UOStx) {
send_char(LETTERS);
send_char(0x04); // coded value for a space
txmode = LETTERS;
} else
send_char(0x04);
return 0;
}
if ((c = baudot_enc(c)) < 0)
return 0;
// switch case if necessary
if ((c & 0x300) != txmode) {// == 0) {
if (txmode == FIGURES) {
send_char(LETTERS);
txmode = LETTERS;
} else {
send_char(FIGURES);
txmode = FIGURES;
}
}
send_char(c & 0x1F);
return 0;
}
int rtty::baudot_enc(unsigned char data)
{
int i, c, mode;
mode = 0;
c = -1;
if (islower(data))
data = toupper(data);
for (i = 0; i < 32; i++) {
if (data == letters[i]) {
mode |= LETTERS;
c = i;
}
if (data == figures[i]) {
mode |= FIGURES;
c = i;
}
if (c != -1)
return (mode | c);
}
return -1;
}
char rtty::baudot_dec(unsigned char data)
{
int out = 0;
switch (data) {
case 0x1F: /* letters */
rxmode = LETTERS;
break;
case 0x1B: /* figures */
rxmode = FIGURES;
break;
case 0x04: /* unshift-on-space */
if (progdefaults.UOSrx)
rxmode = LETTERS;
return ' ';
break;
default:
if (rxmode == LETTERS)
out = letters[data];
else
out = figures[data];
break;
}
return out;
}