Subscribe to config changes now possible
The receiver configuration variables SQL_HANGTIME, SQL_EXTENDED_HANGTIME and SQL_EXTENDED_HANGTIME_THRESH are now settable from within a C++ module for example. This is made possible by the addition of the Config::valueUpdated signal that is emitted when the Config::setValue function is called.
This commit is contained in:
parent
2c20d274ec
commit
92ef3e45f2
|
|
@ -18,6 +18,9 @@
|
|||
|
||||
* AudioSelector::autoSelectEnabled() can now be called with a constant source.
|
||||
|
||||
* Add a signal to the Config class so that one can subscribe to changes in the
|
||||
configuration.
|
||||
|
||||
|
||||
|
||||
1.6.0 -- 01 Sep 2019
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ using namespace Async;
|
|||
|
||||
Config::~Config(void)
|
||||
{
|
||||
//fclose(file);
|
||||
} /* Config::~Config */
|
||||
|
||||
|
||||
|
|
@ -133,19 +132,19 @@ bool Config::open(const string& name)
|
|||
{
|
||||
errno = 0;
|
||||
|
||||
file = fopen(name.c_str(), "r");
|
||||
FILE *file = fopen(name.c_str(), "r");
|
||||
if (file == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = parseCfgFile();
|
||||
|
||||
|
||||
bool success = parseCfgFile(file);
|
||||
|
||||
fclose(file);
|
||||
file = NULL;
|
||||
|
||||
|
||||
return success;
|
||||
|
||||
|
||||
} /* Config::open */
|
||||
|
||||
|
||||
|
|
@ -186,7 +185,6 @@ const string &Config::getValue(const string& section, const string& tag) const
|
|||
}
|
||||
|
||||
return val_it->second;
|
||||
|
||||
} /* Config::getValue */
|
||||
|
||||
|
||||
|
|
@ -227,10 +225,10 @@ void Config::setValue(const std::string& section, const std::string& tag,
|
|||
{
|
||||
Values &values = sections[section];
|
||||
values[tag] = value;
|
||||
valueUpdated(section, tag);
|
||||
} /* Config::setValue */
|
||||
|
||||
|
||||
|
||||
/****************************************************************************
|
||||
*
|
||||
* Protected member functions
|
||||
|
|
@ -275,7 +273,7 @@ void Config::setValue(const std::string& section, const std::string& tag,
|
|||
* Bugs:
|
||||
*----------------------------------------------------------------------------
|
||||
*/
|
||||
bool Config::parseCfgFile(void)
|
||||
bool Config::parseCfgFile(FILE *file)
|
||||
{
|
||||
char line[16384];
|
||||
int line_no = 0;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ An example of how to use the Config class
|
|||
****************************************************************************/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sigc++/sigc++.h>
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
|
@ -139,7 +140,7 @@ class Config
|
|||
/**
|
||||
* @brief Default constuctor
|
||||
*/
|
||||
Config(void) : file(NULL) {}
|
||||
Config(void) {}
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
|
|
@ -413,13 +414,26 @@ class Config
|
|||
* is created.
|
||||
* Note that this function will not write anything back to the
|
||||
* associated configuration file. It will only set the value in memory.
|
||||
*
|
||||
* The valueUpdated signal will be emitted so that subscribers can get
|
||||
* notified when the value of a configuration variable is changed.
|
||||
*/
|
||||
void setValue(const std::string& section, const std::string& tag,
|
||||
const std::string& value);
|
||||
|
||||
|
||||
/**
|
||||
* @brief A signal that is emitted when a config value is updated
|
||||
* @param section The config section of the update
|
||||
* @param tag The tag (variable name) of the update
|
||||
*
|
||||
* This signal is emitted whenever a configuration variable is changed
|
||||
* by calling the setValue function.
|
||||
*/
|
||||
sigc::signal<void, const std::string&, const std::string&> valueUpdated;
|
||||
|
||||
private:
|
||||
typedef std::map<std::string, std::string> Values;
|
||||
typedef std::map<std::string, Values> Sections;
|
||||
typedef std::map<std::string, std::string> Values;
|
||||
typedef std::map<std::string, Values> Sections;
|
||||
struct csv_whitespace : std::ctype<char>
|
||||
{
|
||||
static const mask* make_table(void)
|
||||
|
|
@ -434,10 +448,11 @@ class Config
|
|||
: std::ctype<char>(make_table(), false, refs) {}
|
||||
};
|
||||
|
||||
FILE *file;
|
||||
Sections sections;
|
||||
|
||||
bool parseCfgFile(void);
|
||||
|
||||
//Config(const Config&);
|
||||
//Config& operator=(const Config&);
|
||||
bool parseCfgFile(FILE *file);
|
||||
char *trimSpaces(char *line);
|
||||
char *parseSection(char *line);
|
||||
char *parseDelimitedString(char *str, char begin_tok, char end_tok);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@
|
|||
* ModuleEchoLink: Now possible to silently ignore all incoming connections
|
||||
using the DROP_ALL_INCOMING configuration variable.
|
||||
|
||||
* The receiver configuration variables SQL_HANGTIME, SQL_EXTENDED_HANGTIME and
|
||||
SQL_EXTENDED_HANGTIME_THRESH are now settable from within a C++ module for
|
||||
example.
|
||||
|
||||
|
||||
|
||||
1.7.0 -- 01 Sep 2019
|
||||
|
|
|
|||
|
|
@ -166,12 +166,12 @@ class NetTrxAdapter
|
|||
private:
|
||||
static std::map<std::string, NetTrxAdapter*> net_trx_adapters;
|
||||
|
||||
Uplink *ul;
|
||||
Async::Config cfg;
|
||||
TxAdapter *txa1;
|
||||
TxAdapter *txa2;
|
||||
RxAdapter *rxa1;
|
||||
std::string net_uplink_name;
|
||||
Uplink* ul;
|
||||
Async::Config& cfg;
|
||||
TxAdapter* txa1;
|
||||
TxAdapter* txa2;
|
||||
RxAdapter* rxa1;
|
||||
std::string net_uplink_name;
|
||||
|
||||
NetTrxAdapter(const NetTrxAdapter&);
|
||||
NetTrxAdapter& operator=(const NetTrxAdapter&);
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class DtmfDecoder : public sigc::trackable, public Async::AudioSink
|
|||
private:
|
||||
static const unsigned DEFAULT_HANGTIME = 0;
|
||||
|
||||
Async::Config m_cfg;
|
||||
Async::Config& m_cfg;
|
||||
std::string m_name;
|
||||
unsigned m_hangtime;
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,12 @@ class AudioUdpSink : public UdpSocket, public AudioSink
|
|||
*
|
||||
****************************************************************************/
|
||||
|
||||
namespace {
|
||||
typedef const char *CfgTag;
|
||||
CfgTag CFG_SQL_HANGTIME = "SQL_HANGTIME";
|
||||
CfgTag CFG_SQL_EXTENDED_HANGTIME = "SQL_EXTENDED_HANGTIME";
|
||||
CfgTag CFG_SQL_EXTENDED_HANGTIME_THRESH = "SQL_EXTENDED_HANGTIME_THRESH";
|
||||
};
|
||||
|
||||
|
||||
/****************************************************************************
|
||||
|
|
@ -237,7 +243,7 @@ class AudioUdpSink : public UdpSocket, public AudioSink
|
|||
****************************************************************************/
|
||||
|
||||
LocalRxBase::LocalRxBase(Config &cfg, const std::string& name)
|
||||
: Rx(cfg, name), cfg(cfg), mute_state(MUTE_ALL),
|
||||
: Rx(cfg, name), mute_state(MUTE_ALL),
|
||||
squelch_det(0), siglevdet(0), /* siglev_offset(0.0), siglev_slope(1.0), */
|
||||
tone_dets(0), sql_valve(0), delay(0), sql_tail_elim(0),
|
||||
preamp_gain(0), mute_valve(0), sql_hangtime(0), sql_extended_hangtime(0),
|
||||
|
|
@ -267,25 +273,25 @@ bool LocalRxBase::initialize(void)
|
|||
}
|
||||
|
||||
bool deemphasis = false;
|
||||
cfg.getValue(name(), "DEEMPHASIS", deemphasis);
|
||||
cfg().getValue(name(), "DEEMPHASIS", deemphasis);
|
||||
|
||||
int delay_line_len = 0;
|
||||
bool mute_1750 = false;
|
||||
if (cfg.getValue(name(), "1750_MUTING", mute_1750))
|
||||
if (cfg().getValue(name(), "1750_MUTING", mute_1750))
|
||||
{
|
||||
delay_line_len = max(delay_line_len, TONE_1750_MUTING_PRE);
|
||||
}
|
||||
|
||||
cfg.getValue(name(), "SQL_TAIL_ELIM", sql_tail_elim);
|
||||
cfg().getValue(name(), "SQL_TAIL_ELIM", sql_tail_elim);
|
||||
if (sql_tail_elim > 0)
|
||||
{
|
||||
delay_line_len = max(delay_line_len, sql_tail_elim);
|
||||
}
|
||||
|
||||
cfg.getValue(name(), "PREAMP", preamp_gain);
|
||||
cfg().getValue(name(), "PREAMP", preamp_gain);
|
||||
|
||||
bool peak_meter = false;
|
||||
cfg.getValue(name(), "PEAK_METER", peak_meter);
|
||||
cfg().getValue(name(), "PEAK_METER", peak_meter);
|
||||
|
||||
// Get the audio source object
|
||||
AudioSource *prev_src = audioSource();
|
||||
|
|
@ -298,7 +304,7 @@ bool LocalRxBase::initialize(void)
|
|||
prev_src = input_fifo;
|
||||
|
||||
SvxLink::SepPair<string, uint16_t> raw_audio_fwd_dest;
|
||||
if (cfg.getValue(name(), "RAW_AUDIO_UDP_DEST", raw_audio_fwd_dest))
|
||||
if (cfg().getValue(name(), "RAW_AUDIO_UDP_DEST", raw_audio_fwd_dest))
|
||||
{
|
||||
AudioSplitter *raw_audio_splitter = new AudioSplitter;
|
||||
prev_src->registerSink(raw_audio_splitter, true);
|
||||
|
|
@ -348,9 +354,9 @@ bool LocalRxBase::initialize(void)
|
|||
prev_src->registerSink(siglevdet_splitter, true);
|
||||
|
||||
// Create the signal level detector
|
||||
siglevdet = SigLevDetFactoryBase::createNamedSigLevDet(cfg, name());
|
||||
siglevdet = SigLevDetFactoryBase::createNamedSigLevDet(cfg(), name());
|
||||
if ((siglevdet == 0) ||
|
||||
(!siglevdet->initialize(cfg, name(), INTERNAL_SAMPLE_RATE)))
|
||||
(!siglevdet->initialize(cfg(), name(), INTERNAL_SAMPLE_RATE)))
|
||||
{
|
||||
cout << "*** ERROR: Could not initialize the signal level detector for "
|
||||
<< "receiver " << name() << endl;
|
||||
|
|
@ -404,7 +410,7 @@ bool LocalRxBase::initialize(void)
|
|||
|
||||
// Create the configured squelch detector and initialize it
|
||||
string sql_det_str;
|
||||
if (!cfg.getValue(name(), "SQL_DET", sql_det_str))
|
||||
if (!cfg().getValue(name(), "SQL_DET", sql_det_str))
|
||||
{
|
||||
cerr << "*** ERROR: Config variable " << name() << "/SQL_DET not set\n";
|
||||
return false;
|
||||
|
|
@ -459,7 +465,7 @@ bool LocalRxBase::initialize(void)
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!squelch_det->initialize(cfg, name()))
|
||||
if (!squelch_det->initialize(cfg(), name()))
|
||||
{
|
||||
cerr << "*** ERROR: Squelch detector initialization failed for RX \""
|
||||
<< name() << "\"\n";
|
||||
|
|
@ -471,13 +477,13 @@ bool LocalRxBase::initialize(void)
|
|||
|
||||
readyStateChanged.connect(mem_fun(*this, &LocalRxBase::rxReadyStateChanged));
|
||||
|
||||
if (cfg.getValue(name(), "SQL_HANGTIME", sql_hangtime))
|
||||
if (cfg().getValue(name(), CFG_SQL_HANGTIME, sql_hangtime))
|
||||
{
|
||||
squelch_det->setHangtime(sql_hangtime);
|
||||
}
|
||||
cfg.getValue(name(), "SQL_EXTENDED_HANGTIME", sql_extended_hangtime);
|
||||
cfg.getValue(name(), "SQL_EXTENDED_HANGTIME_THRESH",
|
||||
sql_extended_hangtime_thresh);
|
||||
cfg().getValue(name(), CFG_SQL_EXTENDED_HANGTIME, sql_extended_hangtime);
|
||||
cfg().getValue(name(), CFG_SQL_EXTENDED_HANGTIME_THRESH,
|
||||
sql_extended_hangtime_thresh);
|
||||
|
||||
squelch_det->squelchOpen.connect(mem_fun(*this, &LocalRxBase::onSquelchOpen));
|
||||
fullband_splitter->addSink(squelch_det, true);
|
||||
|
|
@ -488,16 +494,16 @@ bool LocalRxBase::initialize(void)
|
|||
// Set up out of band AFSK demodulator if configured
|
||||
float voice_gain = 0.0f;
|
||||
bool ob_afsk_enable = false;
|
||||
if (cfg.getValue(name(), "OB_AFSK_ENABLE", ob_afsk_enable) && ob_afsk_enable)
|
||||
if (cfg().getValue(name(), "OB_AFSK_ENABLE", ob_afsk_enable) && ob_afsk_enable)
|
||||
{
|
||||
unsigned fc = 5500;
|
||||
//cfg.getValue(name(), "OB_AFSK_CENTER_FQ", fc);
|
||||
//cfg().getValue(name(), "OB_AFSK_CENTER_FQ", fc);
|
||||
unsigned shift = 170;
|
||||
//cfg.getValue(name(), "OB_AFSK_SHIFT", shift);
|
||||
//cfg().getValue(name(), "OB_AFSK_SHIFT", shift);
|
||||
unsigned baudrate = 300;
|
||||
//cfg.getValue(name(), "OB_AFSK_BAUDRATE", baudrate);
|
||||
//cfg().getValue(name(), "OB_AFSK_BAUDRATE", baudrate);
|
||||
voice_gain = 6.0f;
|
||||
cfg.getValue(name(), "OB_AFSK_VOICE_GAIN", voice_gain);
|
||||
cfg().getValue(name(), "OB_AFSK_VOICE_GAIN", voice_gain);
|
||||
|
||||
// Frequency sampling filter with passband center 5500Hz, about 400Hz
|
||||
// wide and about 40dB stop band attenuation
|
||||
|
|
@ -532,14 +538,14 @@ bool LocalRxBase::initialize(void)
|
|||
}
|
||||
|
||||
bool ib_afsk_enable = false;
|
||||
if (cfg.getValue(name(), "IB_AFSK_ENABLE", ib_afsk_enable) && ib_afsk_enable)
|
||||
if (cfg().getValue(name(), "IB_AFSK_ENABLE", ib_afsk_enable) && ib_afsk_enable)
|
||||
{
|
||||
unsigned fc = 1700;
|
||||
//cfg.getValue(name(), "IB_AFSK_CENTER_FQ", fc);
|
||||
//cfg().getValue(name(), "IB_AFSK_CENTER_FQ", fc);
|
||||
unsigned shift = 1000;
|
||||
//cfg.getValue(name(), "IB_AFSK_SHIFT", shift);
|
||||
//cfg().getValue(name(), "IB_AFSK_SHIFT", shift);
|
||||
unsigned baudrate = 1200;
|
||||
//cfg.getValue(name(), "IB_AFSK_BAUDRATE", baudrate);
|
||||
//cfg().getValue(name(), "IB_AFSK_BAUDRATE", baudrate);
|
||||
|
||||
AfskDemodulator *fsk_demod =
|
||||
new AfskDemodulator(fc - shift/2, fc + shift/2, baudrate);
|
||||
|
|
@ -580,10 +586,10 @@ bool LocalRxBase::initialize(void)
|
|||
|
||||
// Create the configured type of DTMF decoder and add it to the splitter
|
||||
string dtmf_dec_type("NONE");
|
||||
cfg.getValue(name(), "DTMF_DEC_TYPE", dtmf_dec_type);
|
||||
cfg().getValue(name(), "DTMF_DEC_TYPE", dtmf_dec_type);
|
||||
if (dtmf_dec_type != "NONE")
|
||||
{
|
||||
DtmfDecoder *dtmf_dec = DtmfDecoder::create(this, cfg, name());
|
||||
DtmfDecoder *dtmf_dec = DtmfDecoder::create(this, cfg(), name());
|
||||
if ((dtmf_dec == 0) || !dtmf_dec->initialize())
|
||||
{
|
||||
// FIXME: Cleanup?
|
||||
|
|
@ -597,7 +603,7 @@ bool LocalRxBase::initialize(void)
|
|||
voiceband_splitter->addSink(dtmf_dec, true);
|
||||
|
||||
bool dtmf_muting = false;
|
||||
cfg.getValue(name(), "DTMF_MUTING", dtmf_muting);
|
||||
cfg().getValue(name(), "DTMF_MUTING", dtmf_muting);
|
||||
if (dtmf_muting)
|
||||
{
|
||||
dtmf_muting_pre = dtmf_dec->detectionTime();
|
||||
|
|
@ -607,10 +613,10 @@ bool LocalRxBase::initialize(void)
|
|||
|
||||
// Create a selective multiple tone detector object
|
||||
string sel5_dec_type("NONE");
|
||||
cfg.getValue(name(), "SEL5_DEC_TYPE", sel5_dec_type);
|
||||
cfg().getValue(name(), "SEL5_DEC_TYPE", sel5_dec_type);
|
||||
if (sel5_dec_type != "NONE")
|
||||
{
|
||||
Sel5Decoder *sel5_dec = Sel5Decoder::create(cfg, name());
|
||||
Sel5Decoder *sel5_dec = Sel5Decoder::create(cfg(), name());
|
||||
if (sel5_dec == 0 || !sel5_dec->initialize())
|
||||
{
|
||||
cerr << "*** ERROR: Sel5 decoder initialization failed for RX \""
|
||||
|
|
@ -673,7 +679,7 @@ bool LocalRxBase::initialize(void)
|
|||
// the LocalRxBase class
|
||||
setHandler(prev_src);
|
||||
|
||||
cfg.getValue(name(), "AUDIO_DEV_KEEP_OPEN", audio_dev_keep_open);
|
||||
cfg().getValue(name(), "AUDIO_DEV_KEEP_OPEN", audio_dev_keep_open);
|
||||
|
||||
// Open the audio device for reading
|
||||
if (!audioOpen())
|
||||
|
|
@ -692,8 +698,10 @@ bool LocalRxBase::initialize(void)
|
|||
//cout << "### Enabling 1750Hz muting\n";
|
||||
}
|
||||
|
||||
cfg().valueUpdated.connect(sigc::mem_fun(*this, &LocalRxBase::cfgUpdated));
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
} /* LocalRxBase:initialize */
|
||||
|
||||
|
||||
|
|
@ -999,6 +1007,40 @@ void LocalRxBase::publishSquelchState(void)
|
|||
} /* LocalRxBase::publishSquelchState */
|
||||
|
||||
|
||||
void LocalRxBase::cfgUpdated(const std::string& section, const std::string& tag)
|
||||
{
|
||||
//std::cout << "### LocalRxBase::cfgUpdated: "
|
||||
// << section << "/" << tag << "=" << cfg().getValue(section, tag)
|
||||
// << std::endl;
|
||||
if (section == name())
|
||||
{
|
||||
if (tag == CFG_SQL_HANGTIME)
|
||||
{
|
||||
if (cfg().getValue(name(), CFG_SQL_HANGTIME, sql_hangtime))
|
||||
{
|
||||
squelch_det->setHangtime(sql_hangtime);
|
||||
}
|
||||
std::cout << "Setting " << CFG_SQL_HANGTIME << " to " << sql_hangtime
|
||||
<< " for receiver " << name() << std::endl;
|
||||
}
|
||||
else if (tag == CFG_SQL_EXTENDED_HANGTIME)
|
||||
{
|
||||
cfg().getValue(name(), CFG_SQL_EXTENDED_HANGTIME, sql_extended_hangtime);
|
||||
std::cout << "Setting " << CFG_SQL_EXTENDED_HANGTIME << " to "
|
||||
<< sql_extended_hangtime
|
||||
<< " for receiver " << name() << std::endl;
|
||||
}
|
||||
else if (tag == CFG_SQL_EXTENDED_HANGTIME_THRESH)
|
||||
{
|
||||
cfg().getValue(name(), CFG_SQL_EXTENDED_HANGTIME_THRESH,
|
||||
sql_extended_hangtime_thresh);
|
||||
std::cout << "Setting " << CFG_SQL_EXTENDED_HANGTIME_THRESH << " to "
|
||||
<< sql_extended_hangtime_thresh
|
||||
<< " for receiver " << name() << std::endl;
|
||||
}
|
||||
}
|
||||
} /* LocalRxBase::cfgUpdated */
|
||||
|
||||
|
||||
/*
|
||||
* This file has not been truncated
|
||||
|
|
|
|||
|
|
@ -237,7 +237,6 @@ class LocalRxBase : public Rx
|
|||
virtual Async::AudioSource *audioSource(void) = 0;
|
||||
|
||||
private:
|
||||
Async::Config &cfg;
|
||||
MuteState mute_state;
|
||||
Squelch *squelch_det;
|
||||
SigLevDet *siglevdet;
|
||||
|
|
@ -269,6 +268,7 @@ class LocalRxBase : public Rx
|
|||
void setSqlHangtimeFromSiglev(float siglev);
|
||||
void rxReadyStateChanged(void);
|
||||
void publishSquelchState(void);
|
||||
void cfgUpdated(const std::string& section, const std::string& tag);
|
||||
|
||||
}; /* class LocalRxBase */
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,13 @@ class Rx : public sigc::trackable, public Async::AudioSource
|
|||
* @brief Destructor
|
||||
*/
|
||||
virtual ~Rx(void);
|
||||
|
||||
|
||||
/**
|
||||
* @brief The config object
|
||||
* @returns Returns a reference to the configuration object
|
||||
*/
|
||||
Async::Config& cfg(void) { return m_cfg; }
|
||||
|
||||
/**
|
||||
* @brief Initialize the receiver object
|
||||
* @return Return \em true on success, or \em false on failure
|
||||
|
|
@ -303,11 +309,11 @@ class Rx : public sigc::trackable, public Async::AudioSource
|
|||
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
bool m_verbose;
|
||||
bool m_sql_open;
|
||||
Async::Config m_cfg;
|
||||
Async::Timer *m_sql_tmo_timer;
|
||||
std::string m_name;
|
||||
bool m_verbose;
|
||||
bool m_sql_open;
|
||||
Async::Config& m_cfg;
|
||||
Async::Timer* m_sql_tmo_timer;
|
||||
|
||||
void sqlTimeout(Async::Timer *t);
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class Sel5Decoder : public sigc::trackable, public Async::AudioSink
|
|||
const std::string &name(void) const { return m_name; }
|
||||
|
||||
private:
|
||||
Async::Config m_cfg;
|
||||
Async::Config& m_cfg;
|
||||
std::string m_name;
|
||||
|
||||
}; /* class Sel5Decoder */
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ QTEL=1.2.4
|
|||
LIBECHOLIB=1.3.3
|
||||
|
||||
# Version for the Async library
|
||||
LIBASYNC=1.6.0.99.3
|
||||
LIBASYNC=1.6.0.99.4
|
||||
|
||||
# SvxLink versions
|
||||
SVXLINK=1.7.99.20
|
||||
SVXLINK=1.7.99.21
|
||||
MODULE_HELP=1.0.0
|
||||
MODULE_PARROT=1.1.1
|
||||
MODULE_ECHO_LINK=1.5.99.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue