//////////////////////////////////////////////////////////////////////////////
// A little totally useless gadget:                                         //
//                                                                          //
// A frequency meter for the BC312/BC342 WW-II Signal Corps receivers       //
// with automatic (sort of) IF value addition/subtraction.                  //
//                                                                          //
// Copyright (C) 2026 Giuseppe Perotti, I1EPJ, i1epj@aricasale.it           //
//                                                                          //
// Version 1.0                                                              //
//////////////////////////////////////////////////////////////////////////////
//                                                                          //
// 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.                             //
//                                                                          //
// You should have received a copy of the GNU General Public License        //
// along with this program.  If not, see <http://www.gnu.org/licenses/>.    //
//                                                                          //
// NOTE. The FreqCount library use the microcontroller clock as time base,  //
// so expect fairly large errors. The resonator used is specified with a    //
// frequency error of 0.5% and a temperature drift of 0.3%. The frequency   //
// error can be corrected in software (see FCORR parameter below), the      //
// drift however can't.                                                     //
//////////////////////////////////////////////////////////////////////////////
//                                                                          //
// This program uses the FreqCount library. Author copyright notice follows.//
//                                                                          //
//////////////////////////////////////////////////////////////////////////////

/* FreqCount Library, for measuring frequencies
 * http://www.pjrc.com/teensy/td_libs_FreqCount.html
 * Copyright (c) 2014 PJRC.COM, LLC - Paul Stoffregen <paul@pjrc.com>
 *
 * Version 1.1
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <FreqCount.h>
#include <EEPROM.h>
#include <stdlib.h>

// Configuration for BC312/BC342.
// All frequency values are in 100 Hz units e.g. 1500 kHz = 15000)
#define DEF_SWITCH_FREQ 80000   // frequency at which LO switches from above
                                // to under the received frequency (8000 kHz)
#define DEF_IF_VALUE 4700       // IF frequency value (470 kHz)
#define DEF_MIN_FREQ 15000      // Minimum value of RX frequency (1500 kHz)
#define DEF_MAX_FREQ 180000     // Maximum value of RX frequency (18000 kHz)
#define DEF_FCORR 0             // frequency correction factor in ppm to
                                // compensate for resonator frequency error
                                // To be determined sperimentally.
                                // On my Arduino UNO R3 it is -679 ppm
#define DEF_BAND_CH_THR 80000   // Threshold past which a band change is
                                // detected.
// Configuration end ---------------------------------------------

// Compilation options
#define SERIAL_CONF             // Enable USB/serial configuration commands
#define HELP_MENU               // Enable help menu command (?)
#undef DEBUG_DISPLAY            // Do not include the D command (display debug)

// 
// Hic sunt leones
//
#define DELAY delay(2)          // If needed can be reduced to 1

#ifdef SERIAL_CONF         
#define MAGIC 0xAA              // Magic value for EEPROM saved conf data
#define MAGICADDRESS 0xAA       // Magic address
#define COPYR \
"\nBC312/BC342 frequency counter v1.0\n\
Copyright (C) 2026 I1EPJ, i1epj@aricasale.it\n\
Distributed under the GNU GPL v3 or later\n"
#endif

#ifdef DEBUG_DISPLAY
char tdi[6];
#endif

// Segments outputs
#define SEG_A 7
#define SEG_B 8
#define SEG_C 9
#define SEG_D 10
#define SEG_E 11
#define SEG_F 12
#define SEG_G 13
// Decimal point
#define DP A2

// Digits outputs
#define DISP_1 A0  // Least significant
#define DISP_2 A1
#define DISP_3 2
#define DISP_4 3
#define DISP_5 4
#define DISP_6 6

//
// Function prototypes
//
void displayStringTimed(const char* str, unsigned int displayTime);
char bcdASCIIto7seg(char digit);
void putDigit(int digitValue, int num);
void selectDigit(int num);
void DisplayOFF(void);

unsigned long count[16];
unsigned long SWF, IF, FMIN, FMAX, BCHTH;
unsigned long countswl, countswh, minLOcount, maxLOcount, bandchthr;
long FCORR, fcorr;
long freq, offsetIF;
int digit, i;
char DisplayImage[6];

// Error messages
char* ov = "Ovrlap";
char* ns = "No sig";
char* fl = "Frq LO";
char* fh = "Frq HI";

#ifdef HELP_MENU
#define HELP \
"Available commands:\n\
? This help text\n\
L Display the copyright and license terms.\n\
M Display/set the minimum frequency.\n\
H Display/set the highest frequency.\n\
I Display/set the IF frequency value.\n\
C Display/set the frequency correction factor in ppm.\n\
S Display/set the frequency at which OL switches\b\
  from above to under RX frequency.\n\
B Display/set the band change detection threshold.\n\
W Save configuration parameters in EEPROM.\n\
T Perform a display test.\n\n"
#endif

#ifdef SERIAL_CONF
int nc;
char s[20];
#endif

// Initializations
void setup() {
  pinMode(A0, OUTPUT);
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  // Pin 5 used by FreqCount
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);

  // Blank display
  DisplayOFF();           // All digit unlit
  digitalWrite(DP, LOW);  // And decimal point too

  // Start FreqCount library
  FreqCount.begin(160); // 160 ms gate time = frequency in 100 Hz units * 16 

  #ifdef SERIAL_CONF
  Serial.begin(115200);
  Serial.setTimeout(5000);

  if (EEPROM.read(MAGICADDRESS) == MAGIC) {
    // if MAGICADDRESS contains MAGICVALUE we have
    // saved configuration in EEPROM, read it
    EEPROM.get(0, SWF);
    EEPROM.get(4, IF);
    EEPROM.get(8, FMIN);
    EEPROM.get(12, FMAX);
    EEPROM.get(16, BCHTH);
    EEPROM.get(20, FCORR);
  } else {
    // init with default values
    SWF = DEF_SWITCH_FREQ;
    IF = DEF_IF_VALUE;
    FMIN = DEF_MIN_FREQ;
    FMAX = DEF_MAX_FREQ;
    FCORR = DEF_FCORR;
    bandchthr = DEF_BAND_CH_THR;
  }
  #endif

  // Init global variables
  offsetIF = 0;
  countswl = 4 * (SWF - IF);  // we read for 160ms (16x) a frequency divided by 4
  countswh = 4 * (SWF + IF);  // ditto
  bandchthr = 4 * BCHTH;      // Band change threshold count value (4xfreq)
  minLOcount = (4 * (FMIN + IF) * 99) / 100;  // less 1% guard
  maxLOcount = (4 * (FMAX - IF) * 101) / 100; // plus 1% guard
  fcorr = 1000000 + FCORR;
  for (i=0; i<6; i++) DisplayImage[i] = ' ';

  // Display program info and license
  // Each string must be 6 characters long
  displayStringTimed("BC342 counter v1_0", 2000);
  displayStringTimed("[c] 2026 I1EPJ",2000);
  displayStringTimed("License GPL v3", 1000);
  displayStringTimed("ready ", 1000);
  digitalWrite(DP, HIGH);

}

// Set all displays off
void DisplayOFF(void) {
  digitalWrite(DISP_1, LOW);
  digitalWrite(DISP_2, LOW);
  digitalWrite(DISP_3, LOW);
  digitalWrite(DISP_4, LOW);
  digitalWrite(DISP_5, LOW);
  digitalWrite(DISP_6, LOW);
}

// Converts a BCD or ASCII value to 7-segment binary value
char bcdASCIIto7seg(char digit) {

  // 7-segment display segment names
  //  ---A---
  // |       |
  // F       B
  // |       |
  //  ---G---
  // |       |
  // E       C
  // |       |
  //  ---D---
  switch (digit) {
    // Digits as BCD or ASCII value
    //                  ABCDEFG
    case 0:
    case '0': return 0b01111110;
    case 1:  
    case '1': return 0b00110000;
    case 2:
    case '2': return 0b01101101;
    case 3:
    case '3': return 0b01111001;
    case 4:
    case '4': return 0b00110011;
    case 5:
    case '5': return 0b01011011;
    case 6:
    case '6': return 0b01011111;
    case 7:
    case '7': return 0b01110000;
    case 8:
    case '8': return 0b01111111;
    case 9:
    case '9': return 0b01111011;

    // Other characters
    //                 ABCDEFG
    case ' ': return 0b00000000;
    case '-': return 0b00000001;
    case '_': return 0b00001000;
    case '=': return 0b00001001;
    case '"': return 0b00100010;
    case '^': return 0b01100011; // degrees really
    case '[': return 0b01001110;
    case ']': return 0b01111000;
    case '\'': return 0b00000010;
    case '?': return 0b01100101;
    case 'A': return 0b01110111;
    case 'a': return 0b01111101;
    case 'B': // one version only possible
    case 'b': return 0b00011111;
    case 'C': return 0b01001110;
    case 'c': return 0b00001101;
    case 'D': // one version only possible
    case 'd': return 0b00111101;
    case 'E': return 0b01001111;
    case 'e': return 0b01101111;
    case 'F': // one version only possible
    case 'f': return 0b01000111;
    case 'G': return 0b01011110;
    case 'g': return 0b01111011;
    case 'H': return 0b00110111;
    case 'h': return 0b00010111;
    case 'I': return 0b00000110;
    case 'i': return 0b00000100;
    case 'J': // one version only possible
    case 'j': return 0b00111100;
    // k is not displayable
    case 'L': return 0b00001110;
    case 'l': return 0b00000110;
    // m is not displayable
    case 'N': return 0b01110110;  // better than norhing
    case 'n': return 0b00010101;  // better than nothing
    case 'O': return 0b01111110;
    case 'o': return 0b00011101;
    case 'P': // one version only possible
    case 'p': return 0b01100111;
    case 'Q': // one version only possible
    case 'q': return 0b01110011;
    case 'R': // one version only possible
    case 'r': return 0b00000101;  // better than nothing
    case 'S': // one version only possible
    case 's': return 0b01011011;
    case 'T': // one version only possible
    case 't': return 0b00001111;  // better than nothing
    case 'U': return 0b00111110;
    case 'u': return 0b00011100;
    case 'v': return 0b00011100;  // v is not displayable, use a u instead
    // w is not displayable
    // x is not displayable
    case 'Y': // one version only possible
    case 'y': return 0b00111011;
    // z is not displayable
    default: return 0b00000000;
  }
}

// Selects a digit (num is 0 to 5, 0 is is leftmost, i.e. most significant, digit)
void selectDigit(int num) {
  DisplayOFF();
  switch (num) {
    case 0:
      {
        digitalWrite(DISP_6, HIGH);
        break;
      }
    case 1:
      {
        digitalWrite(DISP_5, HIGH);
        break;
      }
    case 2:
      {
        digitalWrite(DISP_4, HIGH);
        break;
      }
    case 3:
      {
        digitalWrite(DISP_3, HIGH);
        break;
      }
    case 4:
      {
        digitalWrite(DISP_2, HIGH);
        break;
      }
    case 5:
      {
        digitalWrite(DISP_1, HIGH);
        break;
      }
  }
  DELAY;
}

// Set segment outputs to display digitValue
// and activate required display (num is 0 to 5)
void putDigit(char digitValue, int num) {

  char code7seg;

  // Convert BCD value or ASCII character to 7 segment
  code7seg = bcdASCIIto7seg(digitValue);

  // Put all digits off
  DisplayOFF();

  // Activate/Deactivate segment outputs
  digitalWrite(SEG_A, (code7seg & 0b01000000) ? HIGH : LOW);
  digitalWrite(SEG_B, (code7seg & 0b00100000) ? HIGH : LOW);
  digitalWrite(SEG_C, (code7seg & 0b00010000) ? HIGH : LOW);
  digitalWrite(SEG_D, (code7seg & 0b00001000) ? HIGH : LOW);
  digitalWrite(SEG_E, (code7seg & 0b00000100) ? HIGH : LOW);
  digitalWrite(SEG_F, (code7seg & 0b00000010) ? HIGH : LOW);
  digitalWrite(SEG_G, (code7seg & 0b00000001) ? HIGH : LOW);
   // And then activate the required 7-segment display
  selectDigit(num);
}

// Display a string for a given time in milliseconds.
// If the string is more that 6 characters long, scroll it
// and take all the time needed to show it fully.
// Used only at program start for copyright and license messsages
// and for the T command.
void displayStringTimed(const char* str, unsigned int displayTime) {

  int i,p,l;
  long m1, m2, et;

  m1 = millis();
  l = strlen(str);

  do {
    if (l < 7) {
      for (i = 0; i < 6; i++) {
        putDigit(str[i], i);
        DELAY;
      }
    } else {
      // shift string in display
      m2 = millis();
      for (p=0; p<l-5;) {
        for (i=0; i < 6; i++) {
          putDigit(str[i+p], i);
          DELAY;    
        }
        if ((millis() - m2) > 300) {
          p++;
          m2 = millis();
        }
      }
    }
  } while ((millis() - m1) < displayTime);
}

// Main loop.
// Keep it not too heavy, since to avoid display flickering it must
// execute at least 25 times per second. With the actual code the main loop
// executes about 80 times per second on a Arduino UNO R3, so there is ample
// room for expansion.
//
// To verify what the actual refresh rate is, put a scope or a frequency meter
// on one of the display outputs (DISP_1...DISP_6).
void loop() {
  if (FreqCount.available()) {
    // If a new measure is available, shift previous values, read it
    // and apply frequency correction factor
    for (i = 0; i < 15; i++) count[i + 1] = count[i];

    // Apply frequency correction. We must use a 64 bit integer because
    // the following calculation does not fit into a long integer
    uint64_t ftmp = ((uint64_t)FreqCount.read() * fcorr) / 1000000;

    // Store new value
    count[0] = ftmp;

    // set IF frequency offset, if known (see below)
    if (count[0] < countswl) offsetIF = -IF;
    if (count[0] > countswh) offsetIF = IF;
  
    // if we change band, the offset may change or become undefined, so zero it.
    // To detect band switching, check if the count has suddenly changed more
    // than 2MHz (= 20000 100Hz units times 4). e.g. we have switched from band 3
    // at 5 MHz to band 4 at 8 MHz.
    long fdiff = count[15] - count[0];
    if (abs(fdiff) > bandchthr) offsetIF = 0;
       
    // Calculate receive frequency by averaging 16 readings
    if ((count[0] > minLOcount) && (count[0] < maxLOcount) && (offsetIF != 0)) {
      // Sum 16 readings of f*16 = f*256
      for (i=0; i<16; i++) freq += count[i];

      // Average readings shifting right result 6 places
      freq = (freq >> 6) + offsetIF;

      // Store rightmost 5 digits (always significative)
      // DisplayImage is  [0] [1] [2] [3] [4] [5]
      //                  MSD|---|---|---|---|LSD
      for (i = 5; i > 0; i--) {
        digit = freq % 10;
        freq /= 10;
        DisplayImage[i] = digit;
      }

      // If MSB is zero store a space instead of the digit value.
      // No need to check other digits since the minimum frequency
      // the BC312/342 is capable of is 1500kHz.
      digit = freq;  // only MSD digit remains
      if (digit == 0) {
        // blank leading not significant zero if freq < 10 MHz
        DisplayImage[0] = ' ';
      } else {
        // store digit
        DisplayImage[0] = digit;
      }
      digitalWrite(DP,HIGH);

    } else {
      // At start or when switching band, if LO frequency is in the overlapping
      // range shown below, we do not know which offset apply, so display "Ovrlap"
      //
      // Band 3 (5-8 MHz, fLO=fRX+470 kHz)
      //   5470                           8470
      //    |------------------------------|
      //                       ///////////// <-- OVERLAP!
      //                       |------------------------------|
      //                      7530                          10530
      //                       Band 4 (8-11 MHz, fLO=fRX-470 kHz)
      //
      // To summarize:
      //   if fLO < 7530 kHz, the offset is certainly -470 kHz (fLO=fRX+470kHz)
      //   if fLO > 8470 kHz, the offset is certainly +470 kHz (fLO=fRX-470kHz)
      //   if 7530 kHz < fLO < 8470 kHz, the program can't tell what it is.
      // To make it know, just temporarily tune the LO outside that range,
      // i.e. under 7530kHz (fRX=7060kHz on band 3) or above 8470kHz
      // (fRX=8940kHz on band 4).
      //
      if (offsetIF == 0) {
        digitalWrite(DP, LOW);
        for (i = 0; i < 6; i++) DisplayImage[i] = ov[i];
      }
      // IF count[0] is < 1000 signal input is either absent or too low,
      // so display "No sig"
      if (count[0] < 1000) {
        digitalWrite(DP, LOW);
        for (i=0; i<6; i++) DisplayImage[i] = ns[i];
      } else {
        // We have a reasonable count, but it is too low or too high
        // for a BC312/BC342, so display "Frq LO" or "Frq HI".
        if (count[0] < minLOcount) {
          digitalWrite(DP, LOW);
          for (i = 0; i < 6; i++) DisplayImage[i] = fl[i];
        }
        if (count[0] > maxLOcount) {
          digitalWrite(DP, LOW);
          for (i = 0; i < 6; i++) DisplayImage[i] = fh[i];
        }
      }
    }
  }

#ifdef DEBUGDISPLAY
  for (i = 0; i < 6; i++) DisplayImage[i] = tdi[i];
  digitalWrite(DP, LOW);
#endif

  // And finally display the result
  for (i = 0; i < 6; i++) {
    putDigit(DisplayImage[i], i);
  }

  #ifdef SERIAL_CONF
  // Configuration commands handling
  if (Serial.available()) {
    // Put all displays OFF since display scan stops
    // at first character typed and resumes only after the
    // terminating CR.
    DisplayOFF();
    // Read a CR-terminated command
    nc=Serial.readBytesUntil('\r',s, 19);
    for (i=0; i<nc; i++) {
      if (s[i] < 32) s[i]='\0';
    }
    s[nc]='\0';
    // Command parser
    // Commands can be issued either upper or lower case
    // All frequencies are in 100 Hz units, e.g. 1500 kHz = 15000
    switch (toupper(s[0])) {
      case '\0':
        // no command, CR only, display prompt
        Serial.print("\ncmd:");
        break;
      case 'B':
        // band change threshold - syntax is B [<frequency threshold>]
        if (s[1] != '\0') BCHTH = atol(&s[2]);
          Serial.print("\nBand change threshod: ");
          Serial.println(BCHTH);
        break;
      case 'S':
        // Display/set switch OL frequency - syntax is S [<switch OL frequency>]
        if (s[1] != '\0') SWF = atol(&s[2]);
        Serial.print("\nSwitch frequency: ");
        Serial.println(SWF);
        break;
      case 'I': // IF frequency
        // Display/set IF frequency - syntax is I [<IF frequency>]
        if (s[1] != '\0') IF = atol(&s[2]);
        Serial.print("\nIF frequency: ");
        Serial.println(IF);
        break;
      case 'M':
        // Display/set minimum tunable frequency - syntax is M [<minimum frequency>]
        if (s[1] != '\0') FMIN = atol(&s[2]);
        Serial.print("\nMinimum frequency: ");
        Serial.println(FMIN);
        break;
      case 'H':
        // Display/set maximum tunable frequency - syntax is H [<maximum frequency>]
        if (s[1] != '\0') FMAX = atol(&s[2]);
        Serial.print("\nMaximum frequency: ");
        Serial.println(FMAX);
        break;
      case 'C':
        // Display/set frequency correction factor - syntax is C [<correction factor in ppm>]
        if (s[1] != '\0') FCORR = atol(&s[2]);
        Serial.print("\nFrequency correction: ");
        Serial.println(FCORR);
        break;
      case 'W':
        // Save configuration in EEPROM - syntax is W
        EEPROM.write(MAGICADDRESS, MAGIC);
        EEPROM.put(0, SWF);
        EEPROM.put(4, IF);
        EEPROM.put(8, FMIN);
        EEPROM.put(12, FMAX);
        EEPROM.put(16, BCHTH);
        EEPROM.put(20, FCORR);
        Serial.println("\nConfiguration saved");
        break;
      case 'L':
        // Show copyright and license - syntax is L
        Serial.print(COPYR);
        displayStringTimed("GPL v3 ", 1000);
        break;
      case 'T':
        // Display test - synats is T
        Serial.print("\nPerforming display test...\n");
        digitalWrite(DP, LOW);
        displayStringTimed("Start test", 0);
        displayStringTimed("0123456789", 0);        
        displayStringTimed("aAbcCdeEfgGhHiIjlLnoOpqrstuy", 0);
        displayStringTimed("-_^=?'\"[]", 0);
        digitalWrite(DP, HIGH);
        displayStringTimed("888888", 500);
        digitalWrite(DP, LOW);
        displayStringTimed("      ", 500);
        digitalWrite(DP, HIGH);
        displayStringTimed("888888", 500);
        digitalWrite(DP, LOW);
        displayStringTimed("      ", 500);
        digitalWrite(DP, HIGH);
        displayStringTimed("888888", 500);
        digitalWrite(DP, LOW);
        displayStringTimed("      ", 500);
        displayStringTimed("End test", 0);
        Serial.print("\nDisplay test done\n");        
        break;
#ifdef DEBUG_DISPLAY
      case 'D':
      // Display a string - for debug purposes only - syntax is D [<string>]
      // If no string given show current value
        if (s[1] != '\0') {
          for (i=0; i<6; i++) 
             if (i < strlen(s) - 2)
               tdi[i] = s[i+2];
             else
               tdi[i] = ' ';
        }
        Serial.print("Test string: ");
        for (i=0; i<6; i++) Serial.print(tdi[i]);
        Serial.println();
        break;
#endif
#ifdef HELP_MENU
      case '?':
        // Show help
        Serial.println(HELP);
        break;
#endif
    default:
      Serial.println("\nInvalid command\n");
    }

  // Update configuration values
  countswl = 4*(SWF - IF); // see above
  countswh = 4*(SWF + IF); // ditto 
  minLOcount = (4 * (FMIN + IF) * 99) / 100;  // less 1% guard
  maxLOcount = (4 * (FMAX - IF) * 101) / 100; // plus 1% guard
  bandchthr = 4 * BCHTH;      // Band change threshold count value (4xfreq)
  fcorr = 1000000 + FCORR;
  #endif
  }
}