// // 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 */