//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2013, 2014, 2015, 2016, 2019 John Langner, WB2OSZ
//
// 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, see .
//
/*------------------------------------------------------------------
*
* Name: multi_modem.c
*
* Purpose: Use multiple modems in parallel to increase chances
* of decoding less than ideal signals.
*
* Description: The initial motivation was for HF SSB where mistuning
* causes a shift in the audio frequencies. Here, we can
* have multiple modems tuned to staggered pairs of tones
* in hopes that one will be close enough.
*
* The overall structure opens the door to other approaches
* as well. For VHF FM, the tones should always have the
* right frequencies but we might want to tinker with other
* modem parameters instead of using a single compromise.
*
* Originally: The the interface application is in 3 places:
*
* (a) Main program (direwolf.c or atest.c) calls
* demod_init to set up modem properties and
* hdlc_rec_init for the HDLC decoders.
*
* (b) demod_process_sample is called for each audio sample
* from the input audio stream.
*
* (c) When a valid AX.25 frame is found, process_rec_frame,
* provided by the application, in direwolf.c or atest.c,
* is called. Normally this comes from hdlc_rec.c but
* there are a couple other special cases to consider.
* It can be called from hdlc_rec2.c if it took a long
* time to "fix" corrupted bits. aprs_tt.c constructs
* a fake packet when a touch tone message is received.
*
* New in version 0.9:
*
* Put an extra layer in between which potentially uses
* multiple modems & HDLC decoders per channel. The tricky
* part is picking the best one when there is more than one
* success and discarding the rest.
*
* New in version 1.1:
*
* Several enhancements provided by Fabrice FAURE:
*
* Additional types of attempts to fix a bad CRC.
* Optimized code to reduce execution time.
* Improved detection of duplicate packets from
* different fixup attempts.
* Set limit on number of packets in fix up later queue.
*
* New in version 1.6:
*
* FX.25. Previously a delay of a couple bits (or more accurately
* symbols) was fine because the decoders took about the same amount of time.
* Now, we can have an additional delay of up to 64 check bytes and
* some filler in the data portion. We can't simply wait that long.
* With normal AX.25 a couple frames can come and go during that time.
* We want to delay the duplicate removal while FX.25 block reception
* is going on.
*
*------------------------------------------------------------------*/
//#define DEBUG 1
#define DIGIPEATER_C
#include "direwolf.h"
#include
#include
#include
#include
#include
#include "ax25_pad.h"
#include "textcolor.h"
#include "multi_modem.h"
#include "demod.h"
#include "hdlc_rec.h"
#include "hdlc_rec2.h"
#include "dlq.h"
#include "fx25.h"
#include "version.h"
#include "ais.h"
// Properties of the radio channels.
static struct audio_s *save_audio_config_p;
// Candidates for further processing.
static struct {
packet_t packet_p;
alevel_t alevel;
int is_fx25; // 1 for FX.25, 0 for regular AX.25.
retry_t retries; // For the old "fix bits" strategy, this is the
// number of bits that were modified to get a good CRC.
// It would be 0 to something around 4.
// For FX.25, it is the number of corrected.
// This could be from 0 thru 32.
int age;
unsigned int crc;
int score;
} candidate[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
//#define PROCESS_AFTER_BITS 2 // version 1.4. Was a little short for skew of PSK with different modem types, optional pre-filter
#define PROCESS_AFTER_BITS 3
static int process_age[MAX_CHANS];
static void pick_best_candidate (int chan);
/*------------------------------------------------------------------------------
*
* Name: multi_modem_init
*
* Purpose: Called at application start up to initialize appropriate
* modems and HDLC decoders.
*
* Input: Modem properties structure as filled in from the configuration file.
*
* Outputs:
*
* Description: Called once at application startup time.
*
*------------------------------------------------------------------------------*/
void multi_modem_init (struct audio_s *pa)
{
int chan;
/*
* Save audio configuration for later use.
*/
save_audio_config_p = pa;
memset (candidate, 0, sizeof(candidate));
demod_init (save_audio_config_p);
hdlc_rec_init (save_audio_config_p);
for (chan=0; chanachan[chan].medium == MEDIUM_RADIO) {
if (save_audio_config_p->achan[chan].baud <= 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf("Internal error, chan=%d, %s, %d\n", chan, __FILE__, __LINE__);
save_audio_config_p->achan[chan].baud = DEFAULT_BAUD;
}
int real_baud = save_audio_config_p->achan[chan].baud;
if (save_audio_config_p->achan[chan].modem_type == MODEM_QPSK) real_baud = save_audio_config_p->achan[chan].baud / 2;
if (save_audio_config_p->achan[chan].modem_type == MODEM_8PSK) real_baud = save_audio_config_p->achan[chan].baud / 3;
process_age[chan] = PROCESS_AFTER_BITS * save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec / real_baud ;
//crc_queue_of_last_to_app[chan] = NULL;
}
}
}
/*------------------------------------------------------------------------------
*
* Name: multi_modem_process_sample
*
* Purpose: Feed the sample into the proper modem(s) for the channel.
*
* Inputs: chan - Radio channel number
*
* audio_sample
*
* Description: In earlier versions we always had a one-to-one mapping with
* demodulators and HDLC decoders.
* This was added so we could have multiple modems running in
* parallel with different mark/space tones to compensate for
* mistuning of HF SSB signals.
* It was also possible to run multiple filters, for the same
* tones, in parallel (e.g. ABC).
*
* Version 1.2: Let's try something new for an experiment.
* We will have a single mark/space demodulator but multiple
* slicers, using different levels, each with its own HDLC decoder.
* We now have a separate variable, num_demod, which could be 1
* while num_subchan is larger.
*
* Version 1.3: Go back to num_subchan with single meaning of number of demodulators.
* We now have separate independent variable, num_slicers, for the
* mark/space imbalance compensation.
* num_demod, while probably more descriptive, should not exist anymore.
*
*------------------------------------------------------------------------------*/
static float dc_average[MAX_CHANS];
int multi_modem_get_dc_average (int chan)
{
// Scale to +- 200 so it will like the deviation measurement.
return ( (int) ((float)(dc_average[chan]) * (200.0f / 32767.0f) ) );
}
__attribute__((hot))
void multi_modem_process_sample (int chan, int audio_sample)
{
int d;
int subchan;
// Accumulate an average DC bias level.
// Shouldn't happen with a soundcard but could with mistuned SDR.
dc_average[chan] = dc_average[chan] * 0.999f + (float)audio_sample * 0.001f;
// Issue 128. Someone ran into this.
//assert (save_audio_config_p->achan[chan].num_subchan > 0 && save_audio_config_p->achan[chan].num_subchan <= MAX_SUBCHANS);
//assert (save_audio_config_p->achan[chan].num_slicers > 0 && save_audio_config_p->achan[chan].num_slicers <= MAX_SLICERS);
if (save_audio_config_p->achan[chan].num_subchan <= 0 || save_audio_config_p->achan[chan].num_subchan > MAX_SUBCHANS ||
save_audio_config_p->achan[chan].num_slicers <= 0 || save_audio_config_p->achan[chan].num_slicers > MAX_SLICERS) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR! Something is seriously wrong in %s %s.\n", __FILE__, __func__);
dw_printf ("chan = %d, num_subchan = %d [max %d], num_slicers = %d [max %d]\n", chan,
save_audio_config_p->achan[chan].num_subchan, MAX_SUBCHANS,
save_audio_config_p->achan[chan].num_slicers, MAX_SLICERS);
dw_printf ("Please report this message and include a copy of your configuration file.\n");
exit (EXIT_FAILURE);
}
/* Formerly one loop. */
/* 1.2: We can feed one demodulator but end up with multiple outputs. */
/* Send same thing to all. */
for (d = 0; d < save_audio_config_p->achan[chan].num_subchan; d++) {
demod_process_sample(chan, d, audio_sample);
}
for (subchan = 0; subchan < save_audio_config_p->achan[chan].num_subchan; subchan++) {
int slice;
for (slice = 0; slice < save_audio_config_p->achan[chan].num_slicers; slice++) {
if (candidate[chan][subchan][slice].packet_p != NULL) {
candidate[chan][subchan][slice].age++;
if (candidate[chan][subchan][slice].age > process_age[chan]) {
if (fx25_rec_busy(chan)) {
candidate[chan][subchan][slice].age = 0;
}
else {
pick_best_candidate (chan);
}
}
}
}
}
}
/*-------------------------------------------------------------------
*
* Name: multi_modem_process_rec_frame
*
* Purpose: This is called when we receive a frame with a valid
* FCS and acceptable size.
*
* Inputs: chan - Audio channel number, 0 or 1.
* subchan - Which modem found it.
* slice - Which slice found it.
* fbuf - Pointer to first byte in HDLC frame.
* flen - Number of bytes excluding the FCS.
* alevel - Audio level, range of 0 - 100.
* (Special case, use negative to skip
* display of audio level line.
* Use -2 to indicate DTMF message.)
* retries - Level of correction used.
* is_fx25 - 1 for FX.25, 0 for normal AX.25.
*
* Description: Add to list of candidates. Best one will be picked later.
*
*--------------------------------------------------------------------*/
void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, int is_fx25)
{
packet_t pp;
assert (chan >= 0 && chan < MAX_CHANS);
assert (subchan >= 0 && subchan < MAX_SUBCHANS);
assert (slice >= 0 && slice < MAX_SUBCHANS);
// Special encapsulation for AIS so it can be treated normally pretty much everywhere else.
if (save_audio_config_p->achan[chan].modem_type == MODEM_AIS) {
char nmea[256];
ais_to_nmea (fbuf, flen, nmea, sizeof(nmea));
char monfmt[276];
snprintf (monfmt, sizeof(monfmt), "AIS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_AIS, nmea);
pp = ax25_from_text (monfmt, 1);
// alevel gets in there somehow making me question why it is passed thru here.
}
else {
pp = ax25_from_frame (fbuf, flen, alevel);
}
if (pp == NULL) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Unexpected internal problem, %s %d\n", __FILE__, __LINE__);
return; /* oops! why would it fail? */
}
/*
* If only one demodulator/slicer, and no FX.25 in progress,
* push it thru and forget about all this foolishness.
*/
if (save_audio_config_p->achan[chan].num_subchan == 1 &&
save_audio_config_p->achan[chan].num_slicers == 1 &&
! fx25_rec_busy(chan)) {
int drop_it = 0;
if (save_audio_config_p->recv_error_rate != 0) {
float r = (float)(rand()) / (float)RAND_MAX; // Random, 0.0 to 1.0
//text_color_set(DW_COLOR_INFO);
//dw_printf ("TEMP DEBUG. recv error rate = %d\n", save_audio_config_p->recv_error_rate);
if (save_audio_config_p->recv_error_rate / 100.0 > r) {
drop_it = 1;
text_color_set(DW_COLOR_INFO);
dw_printf ("Intentionally dropping incoming frame. Recv Error rate = %d per cent.\n", save_audio_config_p->recv_error_rate);
}
}
if (drop_it ) {
ax25_delete (pp);
}
else {
dlq_rec_frame (chan, subchan, slice, pp, alevel, is_fx25, retries, "");
}
return;
}
/*
* Otherwise, save them up for a few bit times so we can pick the best.
*/
if (candidate[chan][subchan][slice].packet_p != NULL) {
/* Plain old AX.25: Oops! Didn't expect it to be there. */
/* FX.25: Quietly replace anything already there. It will have priority. */
ax25_delete (candidate[chan][subchan][slice].packet_p);
candidate[chan][subchan][slice].packet_p = NULL;
}
assert (pp != NULL);
candidate[chan][subchan][slice].packet_p = pp;
candidate[chan][subchan][slice].alevel = alevel;
candidate[chan][subchan][slice].is_fx25 = is_fx25;
candidate[chan][subchan][slice].retries = retries;
candidate[chan][subchan][slice].age = 0;
candidate[chan][subchan][slice].crc = ax25_m_m_crc(pp);
}
/*-------------------------------------------------------------------
*
* Name: pick_best_candidate
*
* Purpose: This is called when we have one or more candidates
* available for a certain amount of time.
*
* Description: Pick the best one and send it up to the application.
* Discard the others.
*
* Rules: We prefer one received perfectly but will settle for
* one where some bits had to be flipped to get a good CRC.
*
*--------------------------------------------------------------------*/
/* This is a suitable order for interleaved "G" demodulators. */
/* Opposite order would be suitable for multi-frequency although */
/* multiple slicers are of questionable value for HF SSB. */
#define subchan_from_n(x) ((x) % save_audio_config_p->achan[chan].num_subchan)
#define slice_from_n(x) ((x) / save_audio_config_p->achan[chan].num_subchan)
static void pick_best_candidate (int chan)
{
int best_n, best_score;
char spectrum[MAX_SUBCHANS*MAX_SLICERS+1];
int n, j, k;
int num_bars = save_audio_config_p->achan[chan].num_slicers * save_audio_config_p->achan[chan].num_subchan;
memset (spectrum, 0, sizeof(spectrum));
for (n = 0; n < num_bars; n++) {
j = subchan_from_n(n);
k = slice_from_n(n);
/* Build the spectrum display. */
if (candidate[chan][j][k].packet_p == NULL) {
spectrum[n] = '_';
}
else if (candidate[chan][j][k].is_fx25) {
// FIXME: using retries both as an enum and later int too.
if ((int)(candidate[chan][j][k].retries) <= 9) {
spectrum[n] = '0' + candidate[chan][j][k].retries;
}
else {
spectrum[n] = '+';
}
}
else if (candidate[chan][j][k].retries == RETRY_NONE) {
spectrum[n] = '|';
}
else if (candidate[chan][j][k].retries == RETRY_INVERT_SINGLE) {
spectrum[n] = ':';
}
else {
spectrum[n] = '.';
}
/* Begining score depends on effort to get a valid frame CRC. */
if (candidate[chan][j][k].packet_p == NULL) {
candidate[chan][j][k].score = 0;
}
else {
if (candidate[chan][j][k].is_fx25) {
candidate[chan][j][k].score = 9000 - 100 * candidate[chan][j][k].retries;
}
else {
/* Originally, this produced 0 for the PASSALL case. */
/* This didn't work so well when looking for the best score. */
/* Around 1.3 dev H, we add an extra 1 in here so the minimum */
/* score should now be 1 for anything received. */
candidate[chan][j][k].score = RETRY_MAX * 1000 - ((int)candidate[chan][j][k].retries * 1000) + 1;
}
}
}
/* Bump it up slightly if others nearby have the same CRC. */
for (n = 0; n < num_bars; n++) {
int m;
j = subchan_from_n(n);
k = slice_from_n(n);
if (candidate[chan][j][k].packet_p != NULL) {
for (m = 0; m < num_bars; m++) {
int mj = subchan_from_n(m);
int mk = slice_from_n(m);
if (m != n && candidate[chan][mj][mk].packet_p != NULL) {
if (candidate[chan][j][k].crc == candidate[chan][mj][mk].crc) {
candidate[chan][j][k].score += (num_bars+1) - abs(m-n);
}
}
}
}
}
best_n = 0;
best_score = 0;
for (n = 0; n < num_bars; n++) {
j = subchan_from_n(n);
k = slice_from_n(n);
if (candidate[chan][j][k].packet_p != NULL) {
if (candidate[chan][j][k].score > best_score) {
best_score = candidate[chan][j][k].score;
best_n = n;
}
}
}
#if DEBUG
text_color_set(DW_COLOR_DEBUG);
dw_printf ("\n%s\n", spectrum);
for (n = 0; n < num_bars; n++) {
j = subchan_from_n(n);
k = slice_from_n(n);
if (candidate[chan][j][k].packet_p == NULL) {
dw_printf ("%d.%d.%d: ptr=%p\n", chan, j, k,
candidate[chan][j][k].packet_p);
}
else {
dw_printf ("%d.%d.%d: ptr=%p, is_fx25=%d, retry=%d, age=%3d, crc=%04x, score=%d %s\n", chan, j, k,
candidate[chan][j][k].packet_p,
candidate[chan][j][k].is_fx25,
(int)(candidate[chan][j][k].retries),
candidate[chan][j][k].age,
candidate[chan][j][k].crc,
candidate[chan][j][k].score,
(n == best_n) ? "***" : "");
}
}
#endif
if (best_score == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Unexpected internal problem, %s %d. How can best score be zero?\n", __FILE__, __LINE__);
}
/*
* send the best one along.
*/
/* Delete those not chosen. */
for (n = 0; n < num_bars; n++) {
j = subchan_from_n(n);
k = slice_from_n(n);
if (n != best_n && candidate[chan][j][k].packet_p != NULL) {
ax25_delete (candidate[chan][j][k].packet_p);
candidate[chan][j][k].packet_p = NULL;
}
}
/* Pass along one. */
j = subchan_from_n(best_n);
k = slice_from_n(best_n);
int drop_it = 0;
if (save_audio_config_p->recv_error_rate != 0) {
float r = (float)(rand()) / (float)RAND_MAX; // Random, 0.0 to 1.0
//text_color_set(DW_COLOR_INFO);
//dw_printf ("TEMP DEBUG. recv error rate = %d\n", save_audio_config_p->recv_error_rate);
if (save_audio_config_p->recv_error_rate / 100.0 > r) {
drop_it = 1;
text_color_set(DW_COLOR_INFO);
dw_printf ("Intentionally dropping incoming frame. Recv Error rate = %d per cent.\n", save_audio_config_p->recv_error_rate);
}
}
if ( drop_it ) {
ax25_delete (candidate[chan][j][k].packet_p);
candidate[chan][j][k].packet_p = NULL;
}
else {
assert (candidate[chan][j][k].packet_p != NULL);
dlq_rec_frame (chan, j, k,
candidate[chan][j][k].packet_p,
candidate[chan][j][k].alevel,
candidate[chan][j][k].is_fx25,
(int)(candidate[chan][j][k].retries),
spectrum);
/* Someone else owns it now and will delete it later. */
candidate[chan][j][k].packet_p = NULL;
}
/* Clear in preparation for next time. */
memset (candidate[chan], 0, sizeof(candidate[chan]));
} /* end pick_best_candidate */
/* end multi_modem.c */