/************************************************************************************ ** ** ** UHSDR Firmware Project ** ** ** **---------------------------------------------------------------------------------** ** ** ** Licence: GNU GPLv3 ** ************************************************************************************/ //********************************************************************************* //** //** Project.........: Read Hand Sent Morse Code (tolerant of considerable jitter) //** //** Copyright (c) 2016 Loftur E. Jonasson (tf3lj [at] arrl [dot] net) //** //** 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 3 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. //** //** The GNU General Public License is available at //** http://www.gnu.org/licenses/ //** //** Substantive portions of the methodology used here to decode Morse Code are found in: //** //** "MACHINE RECOGNITION OF HAND-SENT MORSE CODE USING THE PDP-12 COMPUTER" //** by Joel Arthur Guenther, Air Force Institute of Technology, //** Wright-Patterson Air Force Base, Ohio //** December 1973 //** http://www.dtic.mil/dtic/tr/fulltext/u2/786492.pdf //** //** Platform........: Teensy 3.1 / 3.2 and the Teensy Audio Shield //** //** Initial version.: 0.00, 2016-01-25 Loftur Jonasson, TF3LJ / VE2LJX //** //********************************************************************************* #include "uhsdr_board.h" #include "ui_lcd_layouts.h" #include "ui_driver.h" #include "cw_decoder.h" #include "audio_driver.h" #include "rtty.h" #include "cw_gen.h" #include Goertzel cw_goertzel; cw_config_t cw_decoder_config = { .sampling_freq = 12000.0, .target_freq = 750.0, .speed = 25, // .average = 2, .thresh = 32000, .blocksize = CW_DECODER_BLOCKSIZE_DEFAULT, // .AGC_enable = 0, .noisecancel_enable = 1, .spikecancel = 0, .use_3_goertzels = false, // .snap_enable = true, .snap_enable = false, .show_CW_LED = true, // menu choice whether the user wants the CW LED indicator to be working or not }; static void CW_Decode(void); void CwDecode_Filter_Set() { // set Goertzel parameters for CW decoding AudioFilter_CalcGoertzel(&cw_goertzel, ts.cw_sidetone_freq , // cw_decoder_config.target_freq, cw_decoder_config.blocksize, 1.0, cw_decoder_config.sampling_freq); } //#define SIGNAL_TAU 0.01 #define SIGNAL_TAU 0.1 #define ONEM_SIGNAL_TAU (1.0 - SIGNAL_TAU) #define CW_TIMEOUT 3 // Time, in seconds, to trigger display of last Character received #define ONE_SECOND (12000 / cw_decoder_config.blocksize) // sample rate / decimation rate / block size //#define CW_ONE_BIT_SAMPLE_COUNT (ONE_SECOND / 5.83) // standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM #define CW_ONE_BIT_SAMPLE_COUNT (ONE_SECOND / 58.3) // = 6.4 works ! standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM //#define CW_ONE_BIT_SAMPLE_COUNT (ONE_SECOND / 25.0) // does not work! standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM //#define CW_ONE_BIT_SAMPLE_COUNT (ONE_SECOND / 583.0) // does not work. standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM //#define CW_ONE_BIT_SAMPLE_COUNT (12000 / 5.83) // standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM //#define CW_ONE_BIT_SAMPLE_COUNT (12000) // standard word PARIS has 14 pulses & 14 spaces, assumed: 25WPM // 14bits * 25words per min / (60 sec/min) = 5.83 bits/sec bitrate // (sample_rate / blocksize) / bitrate = samples per bit ! #define CW_SPIKECANCEL_MAX_DURATION 8 // Cancel transients/spikes/drops that have max duration of number chosen. // Typically 4 or 8 to select at time periods of 4 or 8 times 2.9ms. // 0 to deselect. #define CW_SIG_BUFSIZE 256 // Size of a circular buffer of decoded input levels and durations #define CW_DATA_BUFSIZE 40 // Size of a buffer of accumulated dot/dash information. Max is DATA_BUFSIZE-2 // Needs to be significantly longer than longest symbol 'sos'= ~30. // FIXME: replace with true/false (since already defined elsewhere) #define TRUE 1 #define FALSE 0 typedef struct { unsigned state :1; // Pulse or space (sample buffer) OR Dot or Dash (data buffer) unsigned time :31; // Time duration } sigbuf; typedef struct { unsigned initialized :1; // Do we have valid time duration measurements? unsigned dash :1; // Dash flag unsigned wspace :1; // Word Space flag unsigned timeout :1; // Timeout flag unsigned overload :1; // Overload flag } bflags; static bool cw_state; // Current decoded signal state static sigbuf sig[CW_SIG_BUFSIZE]; // A circular buffer of decoded input levels and durations, input from static int32_t sig_lastrx = 0; // Circular buffer in pointer, updated by SignalSampler static int32_t sig_incount = 0; // Circular buffer in pointer, copy of sig_lastrx, used by CW Decode functions static int32_t sig_outcount = 0; // Circular buffer out pointer, used by CW Decode functions static int32_t sig_timer = 0; // Elapsed time of current signal state, dependent // on sample rate, decimation factor and CW_DECODE_BLOCK_SIZE // 48ksps & decimation-by-4 equals 12ksps // if CW_DECODE_BLOCK_SIZE == 32, then we have 12000/32 = 375 blocks per second, which means // one Goertzel magnitude is calculated 375 times a second, which means 2.67ms per timer_stepsize // this is very similar to the original 2.9ms (when using FFT256 in the Teensy 3 original sketch) // DD4WH 2017_09_08 static int32_t timer_stepsize = 1; // equivalent to 2.67ms, see above static int32_t cur_time; // copy of sig_timer static int32_t cur_outcount = 0; // Basically same as sig_outcount, for Error Correction functionality static int32_t last_outcount = 0; // sig_outcount for previous character, used for Error Correction func sigbuf data[CW_DATA_BUFSIZE]; // Buffer containing decoded dot/dash and time information // for assembly into a character static uint8_t data_len = 0; // Length of incoming character data static uint32_t code; // Decoded dot/dash info in pairs of bits, - is encoded as 11, and . is encoded as 10 static bflags b; // Various Operational state flags typedef struct { float32_t pulse_avg; // CW timing variables - pulse_avg is a composite value float32_t dot_avg; float32_t dash_avg; // Dot and Dash Space averages float32_t symspace_avg; float32_t cwspace_avg; // Intra symbol Space and Character-Word Space int32_t w_space; // Last word space time } cw_times_t; static cw_times_t cw_times; // audio signal buffer static float32_t raw_signal_buffer[CW_DECODER_BLOCKSIZE_MAX]; //cw_decoder_config.blocksize]; // RINGBUFFER HELPER MACROS START #define ring_idx_wrap_upper(value,size) (((value) >= (size)) ? (value) - (size) : (value)) #define ring_idx_wrap_zero(value,size) (((value) < (0)) ? (value) + (size) : (value)) /* * @brief adjust index "value" by "change" while keeping it in the ring buffer size limits of "size" * @returns the value changed by adding change to it and doing a modulo operation on it for the ring buffer size. So return value is always 0 <= result < size */ #define ring_idx_change(value,change,size) (change>0 ? ring_idx_wrap_upper((value)+(change),(size)):ring_idx_wrap_zero((value)+(change),(size))) #define ring_idx_increment(value,size) ((value+1) == (size)?0:(value+1)) #define ring_idx_decrement(value,size) ((value) == 0?(size)-1:(value)-1) // Determine number of states waiting to be processed #define ring_distanceFromTo(from,to) (((to) < (from))? ((CW_SIG_BUFSIZE + (to)) - ((from) )) : (to - from)) // RINGBUFFER HELPER MACROS END static void CW_Decode_exe(void) { bool newstate; static float32_t CW_env = 0.0; static float32_t CW_mag = 0.0; static float32_t CW_noise = 0.0; float32_t CW_clipped = 0.0; static float32_t old_siglevel = 0.001; static float32_t speed_wpm_avg = 0.0; float32_t siglevel; // signal level from Goertzel calculation static bool prevstate; // Last recorded state of signal input (mark or space) // 1.) get samples // these are already in raw_signal_buffer // 2.) calculate Goertzel for (uint16_t index = 0; index < cw_decoder_config.blocksize; index++) { AudioFilter_GoertzelInput(&cw_goertzel, raw_signal_buffer[index]); } float32_t magnitudeSquared = AudioFilter_GoertzelEnergy(&cw_goertzel); // I am not sure whether we would need an AGC here, because the audio chain already has an AGC // Now I am sure, we do not need it // 3.) AGC #if 0 float32_t pklvl; // Used for AGC calculations if (cw_decoder_config.AGC_enable) { pklvl = CW_agcvol * CW_vol * magnitudeSquared; // Get level at Goertzel frequency if (pklvl > AGC_MAX_PEAK) CW_agcvol = CW_agcvol * CW_AGC_ATTACK; // Decrease volume if above this level. if (pklvl < AGC_MIN_PEAK) CW_agcvol = CW_agcvol * CW_AGC_DECAY; // Increase volume if below this level. if (CW_agcvol > 1.0) CW_agcvol = 1.0; // Cap max at 1.0 siglevel = CW_agcvol * CW_vol * pklvl; } else #endif { siglevel = magnitudeSquared; } // 4.) signal averaging/smoothing #if 0 static float32_t avg_win[20]; // Sliding window buffer for signal averaging, if used static uint8_t avg_cnt = 0; // Sliding window counter avg_win[avg_cnt] = siglevel; // Add value onto "sliding window" buffer avg_cnt = ring_idx_increment(avg_cnt, cw_decoder_config.average); float32_t lvl = 0; // Multiuse variable for (uint8_t x = 0; x < cw_decoder_config.average; x++) // Average up all values within sliding window { lvl = lvl + avg_win[x]; } siglevel = lvl / cw_decoder_config.average; #else // better use exponential averager for averaging/smoothing here !? Let�s try! // siglevel = siglevel * SIGNAL_TAU + ONEM_SIGNAL_TAU * old_siglevel; // old_siglevel = magnitudeSquared; #endif // 4b.) automatic threshold correction if(cw_decoder_config.use_3_goertzels) { CW_mag = siglevel; CW_env = decayavg(CW_env, CW_mag, (CW_mag > CW_env)? // (CW_ONE_BIT_SAMPLE_COUNT / 4) : (CW_ONE_BIT_SAMPLE_COUNT * 16)); (cw_decoder_config.thresh /1000 / 4) : (cw_decoder_config.thresh /1000 * 16)); CW_noise = decayavg(CW_noise, CW_mag, (CW_mag < CW_noise)? //(CW_ONE_BIT_SAMPLE_COUNT / 4) : (CW_ONE_BIT_SAMPLE_COUNT * 48)); (cw_decoder_config.thresh /1000 / 4) : (cw_decoder_config.thresh /1000 * 48)); CW_clipped = CW_mag > CW_env? CW_env: CW_mag; if (CW_clipped < CW_noise) { CW_clipped = CW_noise; } float32_t v1 = (CW_clipped - CW_noise) * (CW_env - CW_noise) - 0.8 * ((CW_env - CW_noise) * (CW_env - CW_noise)); // 0.85 * ((CW_env - CW_noise) * (CW_env - CW_noise)); // ((CW_env - CW_noise) * (CW_env - CW_noise)); // 0.25 * ((CW_env - CW_noise) * (CW_env - CW_noise)); //lowpass // v1 = RttyDecoder_lowPass(v1, rttyDecoderData.lpfConfig, &rttyDecoderData.lpfData); siglevel = v1 * SIGNAL_TAU + ONEM_SIGNAL_TAU * old_siglevel; old_siglevel = v1; // bool newstate = (siglevel > 0)? false:true; newstate = (siglevel < 0)? false:true; } // 5.) signal state determination //---------------- // Signal State sampling // noise cancel requires at least two consecutive samples to be // of same (changed state) to accept change (i.e. a single sample change is ignored). else { siglevel = siglevel * SIGNAL_TAU + ONEM_SIGNAL_TAU * old_siglevel; old_siglevel = magnitudeSquared; newstate = (siglevel >= cw_decoder_config.thresh); } if(cw_decoder_config.noisecancel_enable) { static bool change; // reads to be the same to confirm a true change if (change == TRUE) { cw_state = newstate; change = FALSE; } else if (newstate != cw_state) { change = TRUE; } } else {// No noise canceling cw_state = newstate; } ads.CW_signal = cw_state; // if(ts.dmod_mode == DEMOD_CW) if(cw_decoder_config.show_CW_LED == true && ts.cw_decoder_enable && ts.dmod_mode == DEMOD_CW) { Board_RedLed(cw_state == true? LED_STATE_ON : LED_STATE_OFF); } // 6.) fill into circular buffer //---------------- // Record state changes and durations onto circular buffer if (cw_state != prevstate) { // Enter the type and duration of the state change into the circular buffer sig[sig_lastrx].state = prevstate; sig[sig_lastrx].time = sig_timer; // Zero circular buffer when at max sig_lastrx = ring_idx_increment(sig_lastrx, CW_SIG_BUFSIZE); sig_timer = 0; // Zero the signal timer. prevstate = cw_state; // Update state } //---------------- // Count signal state timer upwards based on which sampling rate is in effect sig_timer = sig_timer + timer_stepsize; if (sig_timer > ONE_SECOND * CW_TIMEOUT) { sig_timer = ONE_SECOND * CW_TIMEOUT; // Impose a MAXTIME second boundary for overflow time } sig_incount = sig_lastrx; // Current Incount pointer cur_time = sig_timer; // 7.) CW Decode if(ts.cw_decoder_enable && ts.dmod_mode == DEMOD_CW) { CW_Decode(); // Do all the heavy lifting } // calculation of speed of the received morse signal on basis of the standard "PARIS" float32_t spdcalc = 10.0 * cw_times.dot_avg + 4.0 * cw_times.dash_avg + 9.0 * cw_times.symspace_avg + 5.0 * cw_times.cwspace_avg; // update only if initialized and prevent division by zero if(b.initialized == true && spdcalc > 0) { // Convert to Milliseconds per Word float32_t speed_ms_per_word = spdcalc * 1000.0 / (cw_decoder_config.sampling_freq / (float32_t)cw_decoder_config.blocksize); float32_t speed_wpm_raw = (0.5 + 60000.0 / speed_ms_per_word); // calculate words per minute speed_wpm_avg = speed_wpm_raw * 0.3 + 0.7 * speed_wpm_avg; // a little lowpass filtering } else { speed_wpm_avg = 0; // we have no calculated speed, i.e. not synchronized to signal } cw_decoder_config.speed = speed_wpm_avg; // for external use, 0 indicates no signal condition if(ts.txrx_mode == TRX_MODE_TX) { // just to ensure that during RX/TX switching the red LED remains lit in TX_mode Board_RedLed(LED_STATE_ON); } } void CwDecode_RxProcessor(float32_t * const src, int16_t blockSize) { static uint16_t sample_counter = 0; for (uint16_t idx = 0; idx < blockSize; idx++) { raw_signal_buffer[sample_counter] = src[idx]; sample_counter++; } if (sample_counter >= cw_decoder_config.blocksize) { CW_Decode_exe(); sample_counter = 0; } } //------------------------------------------------------------------ // // Initialization Function (non-blocking-style) // Determine Pulse, Dash, Dot and initial // Character-Word time averages // // Input is the circular buffer sig[], including in and out counters // Output is variables containing dot dash and space averages // //------------------------------------------------------------------ static void InitializationFunc(void) { static int16_t startpos, progress; // Progress counter, size = SIG_BUFSIZE static bool initializing = FALSE; // Bool for first time init of progress counter int16_t processed; // Number of states that have been processed float32_t t; // We do timing calculations in floating point // to gain a little bit of precision when low // sampling rate // Set up progress counter at beginning of initialize if (initializing == FALSE) { startpos = sig_outcount; // We start at last processed mark/space progress = sig_outcount; initializing = TRUE; cw_times.pulse_avg = 0; // Reset CW timing variables to 0 cw_times.dot_avg = 0; cw_times.dash_avg = 0; cw_times.symspace_avg = 0; cw_times.cwspace_avg = 0; cw_times.w_space = 0; } // Board_RedLed(LED_STATE_ON); // Determine number of states waiting to be processed processed = ring_distanceFromTo(startpos,progress); if (processed >= 98) { b.initialized = TRUE; // Indicate we're done and return initializing = FALSE; // Allow for correct setup of progress if // InitializaitonFunc is invoked a second time // Board_RedLed(LED_STATE_OFF); } if (progress != sig_incount) // Do we have a new state? { t = sig[progress].time; if (sig[progress].state) // Is it a pulse? { if (processed > 32) // More than 32, getting stable { if (t > cw_times.pulse_avg) { cw_times.dash_avg = cw_times.dash_avg + (t - cw_times.dash_avg) / 4.0; // (e.q. 4.5) } else { cw_times.dot_avg = cw_times.dot_avg + (t - cw_times.dot_avg) / 4.0; // (e.q. 4.4) } } else // Less than 32, still quite unstable { if (t > cw_times.pulse_avg) { cw_times.dash_avg = (t + cw_times.dash_avg) / 2.0; // (e.q. 4.2) } else { cw_times.dot_avg = (t + cw_times.dot_avg) / 2.0; // (e.q. 4.1) } } cw_times.pulse_avg = (cw_times.dot_avg / 4 + cw_times.dash_avg) / 2.0; // Update pulse_avg (e.q. 4.3) } else // Not a pulse - determine character_word space avg { if (processed > 32) { if (t > cw_times.pulse_avg) // Symbol space? { cw_times.cwspace_avg = cw_times.cwspace_avg + (t - cw_times.cwspace_avg) / 4.0; // (e.q. 4.8) } else { cw_times.symspace_avg = cw_times.symspace_avg + (t - cw_times.symspace_avg) / 4.0; // New EQ, to assist calculating Rate } } } progress = ring_idx_increment(progress,CW_SIG_BUFSIZE); // Increment progress counter } } //------------------------------------------------------------------ // // Spike Cancel function // // Optionally selectable in CWReceive.h, used by Data Recognition // function to identify and ignore spikes of short duration. // //------------------------------------------------------------------ bool CwDecoder_IsSpike(uint32_t t) { bool retval = false; if (cw_decoder_config.spikecancel == CW_SPIKECANCEL_MODE_SPIKE) // SPIKE CANCEL // Squash spikes/transients of short duration { retval = t <= CW_SPIKECANCEL_MAX_DURATION; } else if (cw_decoder_config.spikecancel == CW_SPIKECANCEL_MODE_SHORT) // SHORT CANCEL // Squash spikes shorter than 1/3rd dot duration { retval = (3 * t < cw_times.dot_avg) && (b.initialized == TRUE); // Only do this if we are not initializing dot/dash periods } return retval; } float32_t spikeCancel(float32_t t) { static bool spike; if (cw_decoder_config.spikecancel != CW_SPIKECANCEL_MODE_OFF) { if (CwDecoder_IsSpike(t) == true) { spike = TRUE; sig_outcount = ring_idx_increment(sig_outcount, CW_SIG_BUFSIZE); // If short, then do nothing t = 0.0; } else if (spike == TRUE) // Check if last state was a short Spike or Drop { spike = FALSE; // Add time of last three states together. t = t + sig[ring_idx_change(sig_outcount, -1, CW_SIG_BUFSIZE)].time + sig[ring_idx_change(sig_outcount, -2, CW_SIG_BUFSIZE)].time; } } return t; } //------------------------------------------------------------------ // // Data Recognition Function (non-blocking-style) // Decode dots, dashes and spaces and group together // into a character. // // Input is the circular buffer sig[], including in and out counters // Variables containing dot, dash and space averages are maintained, and // output is a data[] buffer containing decoded dot/dash information, a // data_len variable containing length of incoming character data. // The function returns TRUE when further calls will not yield a change or a complete new character has been decoded. // The bool variable the parameter points to is set to true if a new character has been decoded // In addition, b.wspace flag indicates whether long (word) space after char // //------------------------------------------------------------------ bool DataRecognitionFunc(bool* new_char_p) { bool not_done = FALSE; // Return value static bool processed; *new_char_p = FALSE; //----------------------------------- // Do we have a new state to process? if (sig_outcount != sig_incount) { not_done = true; b.timeout = FALSE; // Mainly used by Error Correction Function const float32_t t = spikeCancel(sig[sig_outcount].time); // Get time of the new state // Squash spikes/transients if enabled // Attention: Side Effect -> sig_outcount has been be incremented inside spikeCancel if result == 0, because of this we increment only if not 0 if (t > 0) // not a spike (or spike processing not enabled) { const bool is_markstate = sig[sig_outcount].state; sig_outcount = ring_idx_increment(sig_outcount, CW_SIG_BUFSIZE); // Update process counter //----------------------------------- // Is it a Mark (keydown)? if (is_markstate == true) { processed = FALSE; // Indicate that incoming character is not processed // Determine if Dot or Dash (e.q. 4.10) if ((cw_times.pulse_avg - t) >= 0) // It is a Dot { b.dash = FALSE; // Clear Dash flag data[data_len].state = 0; // Store as Dot cw_times.dot_avg = cw_times.dot_avg + (t - cw_times.dot_avg) / 8.0; // Update cw_times.dot_avg (e.q. 4.6) } //----------------------------------- // Is it a Dash? else { b.dash = TRUE; // Set Dash flag data[data_len].state = 1; // Store as Dash if (t <= 5 * cw_times.dash_avg) // Store time if not stuck key { cw_times.dash_avg = cw_times.dash_avg + (t - cw_times.dash_avg) / 8.0; // Update dash_avg (e.q. 4.7) } } data[data_len].time = (uint32_t) t; // Store associated time data_len++; // Increment by one dot/dash cw_times.pulse_avg = (cw_times.dot_avg / 4 + cw_times.dash_avg) / 2.0; // Update pulse_avg (e.q. 4.3) } //----------------------------------- // Is it a Space? else { bool full_char_detected = true; if (b.dash == TRUE) // Last character was a dash { b.dash = false; float32_t eq4_12 = t - (cw_times.pulse_avg - ((uint32_t) data[data_len - 1].time - cw_times.pulse_avg) / 4.0); // (e.q. 4.12, corrected) if (eq4_12 < 0) // Return on symbol space - not a full char yet { cw_times.symspace_avg = cw_times.symspace_avg + (t - cw_times.symspace_avg) / 8.0; // New EQ, to assist calculating Rat full_char_detected = false; } else if (t <= 10 * cw_times.dash_avg) // Current space is not a timeout { float32_t eq4_14 = t - (cw_times.cwspace_avg - ((uint32_t) data[data_len - 1].time - cw_times.pulse_avg) / 4.0); // (e.q. 4.14) if (eq4_14 >= 0) // It is a Word space { cw_times.w_space = t; b.wspace = TRUE; } } } else // Last character was a dot { // (e.q. 4.11) if ((t - cw_times.pulse_avg) < 0) // Return on symbol space - not a full char yet { cw_times.symspace_avg = cw_times.symspace_avg + (t - cw_times.symspace_avg) / 8.0; // New EQ, to assist calculating Rate full_char_detected = false; } else if (t <= 10 * cw_times.dash_avg) // Current space is not a timeout { cw_times.cwspace_avg = cw_times.cwspace_avg + (t - cw_times.cwspace_avg) / 8.0; // (e.q. 4.9) // (e.q. 4.13) if ((t - cw_times.cwspace_avg) >= 0) // It is a Word space { cw_times.w_space = t; b.wspace = TRUE; } } } // Process the character if (full_char_detected == true && processed == FALSE) { *new_char_p = TRUE; // Indicate there is a new char to be processed } } } } //----------------------------------- // Long key down or key up else if (cur_time > (10 * cw_times.dash_avg)) { // If current state is Key up and Long key up then Char finalized if (sig[sig_incount].state == false && processed == false) { processed = TRUE; b.wspace = TRUE; b.timeout = TRUE; *new_char_p = TRUE; // Process the character } } if (data_len > CW_DATA_BUFSIZE - 2) { data_len = CW_DATA_BUFSIZE - 2; // We're receiving garble, throw away } if (*new_char_p) // Update circular buffer pointers for Error function { last_outcount = cur_outcount; cur_outcount = sig_outcount; } return not_done; // FALSE if all data processed or new character, else TRUE } //------------------------------------------------------------------ // // The Code Generation Function converts the received // character to a string code[] of dots and dashes // //------------------------------------------------------------------ void CodeGenFunc() { uint8_t a; code = 0; for (a = 0; a < data_len; a++) { code *= 4; if (data[a].state) { code += 3; // Dash } else { code += 2; // Dit } } data_len = 0; // And make ready for a new Char } void lcdLineScrollPrint(char c) { UiDriver_TextMsgPutChar(c); } //------------------------------------------------------------------ // // The Print Character Function prints to LCD and Serial (USB) // //------------------------------------------------------------------ void PrintCharFunc(uint8_t c) { //-------------------------------------- //-------------------------------------- // Print Characters to LCD //-------------------------------------- // Prosigns if (c == '}') { lcdLineScrollPrint('c'); lcdLineScrollPrint('t'); } else if (c == '(') { lcdLineScrollPrint('k'); lcdLineScrollPrint('n'); } else if (c == '&') { lcdLineScrollPrint('a'); lcdLineScrollPrint('s'); } else if (c == '~') { lcdLineScrollPrint('s'); lcdLineScrollPrint('n'); } else if (c == '>') { lcdLineScrollPrint('s'); lcdLineScrollPrint('k'); } else if (c == '+') { lcdLineScrollPrint('a'); lcdLineScrollPrint('r'); } else if (c == '^') { lcdLineScrollPrint('b'); lcdLineScrollPrint('k'); } else if (c == '{') { lcdLineScrollPrint('c'); lcdLineScrollPrint('l'); } else if (c == '^') { lcdLineScrollPrint('a'); lcdLineScrollPrint('a'); } else if (c == '%') { lcdLineScrollPrint('n'); lcdLineScrollPrint('j'); } else if (c == 0x7f) { lcdLineScrollPrint('e'); lcdLineScrollPrint('r'); lcdLineScrollPrint('r'); } //-------------------------------------- // # is our designated ERROR Symbol else if (c == 0xff) { lcdLineScrollPrint('#'); } //-------------------------------------- // Normal Characters /* if (c == 0xfe || c == 0xff) { lcdLineScrollPrint('#'); } */ else { lcdLineScrollPrint(c); } } //------------------------------------------------------------------ // // The Word Space Function takes care of Word Spaces // to LCD and Serial (USB). // Word Space Correction is applied if certain characters, which // are less likely to be at the end of a word, are received // The characters tested are applicable to the English language // //------------------------------------------------------------------ void WordSpaceFunc(uint8_t c) { if (b.wspace == TRUE) // Print word space { b.wspace = FALSE; // Word space correction routine - longer space required if certain characters if ((c == 'I') || (c == 'J') || (c == 'Q') || (c == 'U') || (c == 'V') || (c == 'Z')) { int16_t x = (cw_times.cwspace_avg + cw_times.pulse_avg) - cw_times.w_space; // (e.q. 4.15) if (x < 0) { lcdLineScrollPrint(' '); } } else { lcdLineScrollPrint(' '); } } } //------------------------------------------------------------------ // // Error Correction Function has three parts // 1) Exits with Error if character is too long (DATA_BUFSIZE-2) // 2) If a dot duration is determined to be less than half expected, // then this dot is eliminated by adding it and the two spaces on // either side to for a new space duration, then new code is generated // for pattern parsing. // 3) If not 2) then separate two run-on characters caused by // a short character space - Extend the char space and reprocess // // If not able to resolve anything, then return FALSE // Return TRUE if something was resolved. // //------------------------------------------------------------------ bool ErrorCorrectionFunc(void) { bool result = FALSE; // Result of Error resolution - FALSE if nothing resolved if (data_len >= CW_DATA_BUFSIZE - 2) // Too long char received { PrintCharFunc(0xff); // Print Error to LCD and Serial (USB) WordSpaceFunc(0xff); // Print Word Space to LCD and Serial when required } else { b.wspace = FALSE; //----------------------------------------------------- // Find the location of pulse with shortest duration // and the location of symbol space of longest duration int32_t temp_outcount = last_outcount; // Grab a copy of endpos for last successful decode int32_t slocation = last_outcount; // Long symbol space duration and location int32_t plocation = last_outcount; // Short pulse duration and location uint32_t pduration = UINT32_MAX; // Very high number to decrement for min pulse duration uint32_t sduration = 0; // and a zero to increment for max symbol space duration // if cur_outcount is < CW_SIG_BUFSIZE, loop must terminate after CW_SIG_BUFSIZE -1 steps while (temp_outcount != cur_outcount) { //----------------------------------------------------- // Find shortest pulse duration. Only test key-down states if (sig[temp_outcount].state) { bool is_shortest_pulse = sig[temp_outcount].time < pduration; // basic test -> shorter than all previously seen ones bool is_not_spike = CwDecoder_IsSpike(sig[temp_outcount].time) == false; if (is_shortest_pulse == true && is_not_spike == true) { pduration = sig[temp_outcount].time; plocation = temp_outcount; } } //----------------------------------------------------- // Find longest symbol space duration. Do not test first state // or last state and only test key-up states if ((temp_outcount != last_outcount) && (temp_outcount != (cur_outcount - 1)) && (!sig[temp_outcount].state)) { if (sig[temp_outcount].time > sduration) { sduration = sig[temp_outcount].time; slocation = temp_outcount; } } temp_outcount = ring_idx_increment(temp_outcount,CW_SIG_BUFSIZE); } uint8_t decoded[] = { 0xff, 0xff }; //----------------------------------------------------- // Take corrective action by dropping shortest pulse // if shorter than half of cw_times.dot_avg // This can result in one or more valid characters - or Error if ((pduration < cw_times.dot_avg / 2) && (plocation != temp_outcount)) { // Add up duration of short pulse and the two spaces on either side, // as space at pulse location + 1 sig[ring_idx_change(plocation, +1, CW_SIG_BUFSIZE)].time = sig[ring_idx_change(plocation, -1, CW_SIG_BUFSIZE)].time + sig[plocation].time + sig[ring_idx_change(plocation, +1, CW_SIG_BUFSIZE)].time; // Shift the preceding data forward accordingly temp_outcount = ring_idx_change(plocation, -2 ,CW_SIG_BUFSIZE); // if last_outcount is < CW_SIG_BUFSIZE, loop must terminate after CW_SIG_BUFSIZE -1 steps while (temp_outcount != last_outcount) { sig[ring_idx_change(temp_outcount, +2, CW_SIG_BUFSIZE)].time = sig[temp_outcount].time; sig[ring_idx_change(temp_outcount, +2, CW_SIG_BUFSIZE)].state = sig[temp_outcount].state; temp_outcount = ring_idx_decrement(temp_outcount,CW_SIG_BUFSIZE); } // And finally shift the startup pointer similarly sig_outcount = ring_idx_change(last_outcount, +2,CW_SIG_BUFSIZE); // // Now we reprocess // // Pull out a character, using the adjusted sig[] buffer // Process character delimited by character or word space bool dummy; while (DataRecognitionFunc(&dummy)) { // nothing } CodeGenFunc(); // Generate a dot/dash pattern string decoded[0] = CwGen_CharacterIdFunc(code); // Convert dot/dash data into a character if (decoded[0] != 0xff) { PrintCharFunc(decoded[0]); result = TRUE; // Error correction had success. } else { PrintCharFunc(0xff); } } //----------------------------------------------------- // Take corrective action by converting the longest symbol space to character space // This will result in two valid characters - or Error else { // Split char in two by adjusting time of longest sym space to a char space sig[slocation].time = ((cw_times.cwspace_avg - 1) >= 1 ? cw_times.cwspace_avg - 1 : 1); // Make sure it is always larger than 0 sig_outcount = last_outcount; // Set circ buffer reference to the start of previous failed decode // // Now we reprocess // // Debug - If timing is out of whack because of noise, with rate // showing at >99 WPM, then DataRecognitionFunc() occasionally fails. // Not found out why, but millis() is used to guards against it. // Process first character delimited by character or word space bool dummy; while (DataRecognitionFunc(&dummy)) { // nothing } CodeGenFunc(); // Generate a dot/dash pattern string decoded[0] = CwGen_CharacterIdFunc(code); // Convert dot/dash pattern into a character // Process second character delimited by character or word space while (DataRecognitionFunc(&dummy)) { // nothing } CodeGenFunc(); // Generate a dot/dash pattern string decoded[1] = CwGen_CharacterIdFunc(code); // Convert dot/dash pattern into a character if ((decoded[0] != 0xff) && (decoded[1] != 0xff)) // If successful error resolution { PrintCharFunc(decoded[0]); PrintCharFunc(decoded[1]); result = TRUE; // Error correction had success. } else { PrintCharFunc(0xff); } } } return result; } //------------------------------------------------------------------ // // CW Decode manages all the decode Functions. // It establishes dot/dash/space periods through the Initialization // function, and when initialized (or if excessive time when not fully // initialized), then it runs DataRecognition, CodeGen and CharacterId // functions to decode any incoming data. If not successful decode // then ErrorCorrection is attempted, and if that fails, then // Initialization is re-performed. // //------------------------------------------------------------------ void CW_Decode(void) { //----------------------------------- // Initialize pulse_avg, dot_avg, cw_times.dash_avg, cw_times.symspace_avg, cwspace_avg if (b.initialized == FALSE) { InitializationFunc(); } //----------------------------------- // Process the works once initialized - or if timeout if ((b.initialized == TRUE) || (cur_time >= ONE_SECOND * CW_TIMEOUT)) // { bool received; // True on a symbol received DataRecognitionFunc(&received); // True if new character received if (received && (data_len > 0)) // also make sure it is not a spike { CodeGenFunc(); // Generate a dot/dash pattern string uint8_t decoded = CwGen_CharacterIdFunc(code); // Identify the Character // 0xff if char not recognized if (decoded < 0xfe) // 0xfe = spike suppression, 0xff = error { PrintCharFunc(decoded); // Print to LCD and Serial (USB) WordSpaceFunc(decoded); // Print Word Space to LCD and Serial when required } else if (decoded == 0xff) // Attempt Error Correction { // If Error Correction function cannot resolve, then reinitialize speed if (ErrorCorrectionFunc() == FALSE) { b.initialized = FALSE; } } } } } void CwDecoder_WpmDisplayClearOrPrepare(bool prepare) { uint16_t color1 = prepare?White:Black; uint16_t color2 = prepare?Green:Black; UiLcdHy28_PrintText(ts.Layout->CW_DECODER_WPM.x, ts.Layout->CW_DECODER_WPM.y," --",color1,Black,0); UiLcdHy28_PrintText(ts.Layout->CW_DECODER_WPM.x + 27, ts.Layout->CW_DECODER_WPM.y, "wpm", color2, Black, 4); if (prepare == true) { CwDecoder_WpmDisplayUpdate(true); } } void CwDecoder_WpmDisplayUpdate(bool force_update) { static uint8_t old_speed = 0; if(cw_decoder_config.speed != old_speed || force_update == true) { char WPM_str[10]; snprintf(WPM_str, 10, cw_decoder_config.speed > 0? "%3u" : " --", cw_decoder_config.speed); UiLcdHy28_PrintText(ts.Layout->CW_DECODER_WPM.x, ts.Layout->CW_DECODER_WPM.y, WPM_str,White,Black,0); } }