/////////////////////////////////////////////////////////////////////////////////////////
//
// UB3APP CW Trainer
// GNU GPLv3 license
// Contact: [email protected]
//
// Forked from:
//
// ARRL's Arduino for Ham Radio by Glen Popiel, KW5GP
// Chapter 17 - Iambic Keyer
//
// Iambic Morse Code Keyer Sketch
// Copyright (c) 2009 Steven T. Elliott
//
// Modified by Glen Popiel, KW5GP
// Based on OpenQRP Blog Iambic Morse Code Keyer Sketch by Steven T. Elliott,
// K1EL - Used with Permission
//
/////////////////////////////////////////////////////////////////////////////////////////
#include <EEPROM.h>
#include <SPI.h>
#include <Wire.h>
// #define SCREEN_TYPE_SSD1306
//#define SCREEN_TYPE_1602
#define SCREEN_TYPE_2004
#ifdef SCREEN_TYPE_SSD1306
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define MAX_SYMB 22
Adafruit_SSD1306 display(128, 32, &Wire, -1);
#endif
#if defined (SCREEN_TYPE_1602) || defined (SCREEN_TYPE_2004)
#include <LiquidCrystal_I2C.h>
#endif
#ifdef SCREEN_TYPE_2004
#define MAX_SYMB 20
LiquidCrystal_I2C lcd(0x27, 20, 4);
#endif
#ifdef SCREEN_TYPE_1602
#define MAX_SYMB 16
LiquidCrystal_I2C lcd(0x27, 16, 2);
#endif
#define ST_Pin 9 // 耳机引脚
#define LP_in 7 // 右边键
#define RP_in 5 // 左边键
#define led_Pin 13 // LED引脚
#define key_Pin 6 // 控制收发机的继电器引脚
#define enc_btn_Pin 2 // 编码器按钮引脚
#define enc_a_Pin 3 // 编码器DT引脚
#define enc_b_Pin 4 // 编码器CLK引脚
int longPressTime = 500; // 按钮长按时间
int blink_time = 500; // 元素闪烁速度
int ST_Freq; // 耳机频率
int key_speed; // 电码速度 WPM
int key_mode = 0; // 电码键模式 - 0 = Iambic Mode A, 1 = Iambic Mode B
struct SettingsObj { // 保存设置到 EEPROM 的结构
int ifreq;
int ispeed;
};
SettingsObj so; // 结构对象
unsigned long ditTime; // 点的持续时间,毫秒
char keyerState; // 电码器状态
enum KSTYPE {IDLE, CHK_DIT, CHK_DAH, KEYED_PREP, KEYED, INTER_ELEMENT};
char keyerControl; // 键控状态
#define DIT_L 0x01 // 点按下
#define DAH_L 0x02 // 划按下
#define DIT_PROC 0x04 // 点正在按下
static long ktimer; // 键定时器
static long ktimer_idle_dit; // 符号之间的时间间隔定时器
static long ktimer_idle_dah; // 单词之间的时间间隔定时器
int idle_first_flag = 4; // 空闲状态
String ch = ""; // 当前摩尔斯码
String text1 = ""; // 摩尔斯码格式的完整文本
String text2 = ""; // 摩尔斯码对应的文本
String text1_prev; // 用于检查文本是否更改的变量
String text2_prev;
const int NUMBER_OF_ELEMENTS = 46; // 符号和代码数组的长度
const int MAX_SIZE = 7; // 摩尔斯码字符串的最大长度
char codecw [NUMBER_OF_ELEMENTS] [MAX_SIZE] = {
{".-"}, {"-..."}, {"-.-."}, {"-.."}, {"."}, {"..-."}, {"--."}, {"...."},
{".."}, {".---"}, {"-.-"}, {".-.."}, {"--"}, {"-."}, {"---"}, {".--."},
{"--.-"}, {".-."}, {"..."}, {"-"}, {"..-"}, {"...-"}, {".--"}, {"-..-"},
{"-.--"}, {"--.."}, {".----"}, {"..---"}, {"...--"}, {"....-"}, {"....."},
{"-...."}, {"--..."}, {"---.."}, {"----."}, {"-----"},
{"..--.."}, {"-..-."}, {".-.-.-"}, {"--..--"}, {"-...-"},
{"---."}, {"----"}, {"..-.."}, {"..--"}, {".-.-"}
};
char symb[NUMBER_OF_ELEMENTS] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890?/.,=cseuj";
int enc_a_PinLast = LOW; // 编码器状态变量
int n = LOW; //
static long encoder_timer = 0; // 编码器定时器
boolean buttonActive = false; // 按钮按下标志
boolean longPressActive = false; // 长按标志
byte menu = 0; // 当前菜单状态
long blink_timer = 0; // 闪烁定时器
byte blink_flag = 0; // 闪烁标志
byte blink_state = 0; // 闪烁状态
void setup() {
Serial.begin(115200);
Wire.setClock(10000000);
init_lcd();
pinMode (enc_a_Pin, INPUT); // 设置编码器引脚
pinMode (enc_b_Pin, INPUT);
pinMode (enc_btn_Pin, INPUT);
digitalWrite(enc_btn_Pin, HIGH); // 启用编码器按钮的上拉电阻
pinMode(led_Pin, OUTPUT); // 设置LED引脚为输出
pinMode(LP_in, INPUT); // 设置右键引脚为输入
pinMode(RP_in, INPUT); // 设置左键引脚为输入
pinMode(ST_Pin, OUTPUT); // 设置耳机引脚为输出
pinMode(key_Pin, OUTPUT); // 设置控制收发机的继电器引脚为输出
digitalWrite(led_Pin, LOW); // 关闭LED
digitalWrite(LP_in, HIGH); // 启用右键上拉电阻
digitalWrite(RP_in, HIGH); // 启用左键上拉电阻
EEPROM.get(0, so);
if ( (so.ispeed == -1) or (so.ifreq == -1) ) {
so.ispeed = 15;
so.ifreq = 800;
EEPROM.put(0, so);
}
ST_Freq = so.ifreq;
key_speed = so.ispeed;
keyerState = IDLE;
print_lcd_menu(true);
}
void init_lcd() {
#ifdef SCREEN_TYPE_SSD1306
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(3);
display.setCursor(14, 0);
display.print("UB3APP");
display.setTextSize(1);
display.setCursor(35, 24);
display.print("CW Trainer");
display.display();
#endif
#ifdef SCREEN_TYPE_1602
lcd.init();
lcd.backlight();
lcd.setCursor(5, 0);
lcd.print("UB3APP");
lcd.setCursor(3, 1);
lcd.print("CW Trainer");
#endif
#ifdef SCREEN_TYPE_2004
lcd.init();
lcd.backlight();
lcd.setCursor(7, 1);
lcd.print("UB3APP");
lcd.setCursor(5, 2);
lcd.print("CW Trainer");
#endif
delay(3000);
#ifdef SCREEN_TYPE_SSD1306
display.setTextSize(1);
display.clearDisplay();
display.display();
#endif
#if defined (SCREEN_TYPE_1602) || defined (SCREEN_TYPE_2004)
lcd.clear();
#endif
}
void loop() {
/*
* WPM set
*/
ditTime = 1200 / key_speed;
/*
* Key state loop
*/
switch (keyerState) {
case IDLE: // 等待直接或锁定的键按下
if ((digitalRead(LP_in) == LOW) || (digitalRead(RP_in) == LOW) || (keyerControl & 0x03)) {
update_PaddleLatch();
keyerState = CHK_DIT;
idle_first_flag = 0;
}
if ((digitalRead(LP_in) != LOW) && (digitalRead(RP_in) != LOW)) {
if (idle_first_flag == 0) {
ktimer_idle_dit = ditTime * 3;
ktimer_idle_dah = ditTime * 7;
idle_first_flag = 1;
}
if (idle_first_flag == 1) {
ktimer_idle_dit += millis();
ktimer_idle_dah += millis();
idle_first_flag = 2;
}
if ( (millis() > ktimer_idle_dit) && idle_first_flag == 2 ) {
Serial.print(code2char(ch));
text1 += " ";
text2 += code2char(ch);
ch = "";
print_lcd();
idle_first_flag = 3;
}
if ( (millis() > ktimer_idle_dah) && idle_first_flag == 3 ) {
Serial.print(" ");
text1 += " ";
text2 += " ";
print_lcd();
idle_first_flag = 4;
}
}
break;
case CHK_DIT: // 检查点键是否被按下
if (keyerControl & DIT_L) {
keyerControl |= DIT_PROC;
ktimer = ditTime;
keyerState = KEYED_PREP;
} else {
keyerState = CHK_DAH;
}
break;
case CHK_DAH: // 检查划键是否被按下
if (keyerControl & DAH_L) {
ktimer = ditTime*3;
keyerState = KEYED_PREP;
} else {
keyerState = IDLE;
}
break;
case KEYED_PREP: // 键按下,开始计时,用于点或划
digitalWrite(led_Pin, HIGH); // 打开LED
tone(ST_Pin, ST_Freq); // 打开耳机
digitalWrite(key_Pin, HIGH); // 打开收发机继电器
if (keyerControl & DIT_L) {
ch += '.';
text1 += '.';
};
if (keyerControl & DAH_L) {
ch += '-';
text1 += '-';
};
ktimer += millis(); // 设置定时器到间隔结束时间
keyerControl &= ~(DIT_L + DAH_L); // 清除两个键的标志位
keyerState = KEYED; // 下一个状态
break;
case KEYED: // 等待定时器到期
if (millis() > ktimer) { // 是否到达按键结束?
digitalWrite(led_Pin, LOW); // 关闭LED
noTone(ST_Pin); // 关闭耳机
digitalWrite(key_Pin, LOW); // 关闭收发机继电器
ktimer = millis() + ditTime; // 插入元素间隔时间
keyerState = INTER_ELEMENT; // 下一个状态
} else { // 检查是否处于意谑模式 B
if (key_mode == 1) update_PaddleLatch(); // 在意谑模式 B 中,提前锁定键
}
break;
case INTER_ELEMENT: // 插入点和划之间的时间
update_PaddleLatch(); // 锁定键状态
if (millis() > ktimer) { // 是否到达间隔结束?
if (keyerControl & DIT_PROC) { // 是否点还是划?
keyerControl &= ~(DIT_L + DIT_PROC); // 清除两个标志位
keyerState = CHK_DAH; // 点完成,检查是否划
} else {
keyerControl &= ~(DAH_L); // clear dah latch
keyerState = IDLE; // go idle
}
}
break;
}
/*
* 编码器旋转部分
*/
n = digitalRead(enc_a_Pin);
if ((enc_a_PinLast == LOW) && (n == HIGH)) {
if (digitalRead(enc_b_Pin) == LOW) {
if (menu == 1) {
key_speed--;
if (key_speed < 5) key_speed = 5;
}
if (menu == 2) {
ST_Freq -= 50;
if (ST_Freq < 400) ST_Freq = 400;
}
if (menu != 0) {
so.ispeed = key_speed;
so.ifreq = ST_Freq;
EEPROM.put(0, so);
}
} else {
if (menu == 1) {
key_speed++;
if (key_speed > 40) key_speed = 40;
}
if (menu == 2) {
ST_Freq += 50;
if (ST_Freq > 1200) ST_Freq = 1200;
}
if (menu != 0) {
so.ispeed = key_speed;
so.ifreq = ST_Freq;
EEPROM.put(0, so);
}
}
}
enc_a_PinLast = n;
/*
* Encoder button part
*/
if (digitalRead(enc_btn_Pin) == LOW) {
if (buttonActive == false) {
buttonActive = true;
encoder_timer = millis();
}
if ((millis() - encoder_timer > longPressTime) && (longPressActive == false)) {
longPressActive = true;
text1 = "";
text2 = "";
print_lcd();
}
} else {
if (buttonActive == true) {
if (longPressActive == true) {
longPressActive = false;
} else {
menu++;
if (menu > 2) {
menu = 0;
print_lcd_menu(true);
}
}
buttonActive = false;
}
}
/*
* Blink timer
*/
if ( blink_flag == 0 ) {
blink_flag = 1;
blink_timer = millis() + blink_time;
}
if ( (blink_flag == 1) && (millis() > blink_timer) ) {
blink_flag = 0;
if ( menu != 0 ) print_lcd_menu(true);
}
}
void update_PaddleLatch() {
if (digitalRead(RP_in) == LOW) keyerControl |= DIT_L;
if (digitalRead(LP_in) == LOW) keyerControl |= DAH_L;
}
String code2char(String c) {
String r = "#";
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++) {
if ( c == codecw[i] ) {
r = symb[i];
break;
}
}
if ( r == "c" ) r = "Ch";
if ( r == "s" ) r = "Sh";
if ( r == "e" ) r = "Ee";
if ( r == "u" ) r = "Ju";
if ( r == "j" ) r = "Ja";
return r;
}
void print_lcd() {
if ( (text1 == text1_prev) && (text2 == text2_prev) ) return;
if ( text1.length() > MAX_SYMB) text1 = text1.substring(text1.length() - MAX_SYMB, text1.length());
if ( text2.length() > MAX_SYMB) text2 = text2.substring(text2.length() - MAX_SYMB, text2.length());
text1_prev = text1;
text2_prev = text2;
#ifdef SCREEN_TYPE_SSD1306
display.clearDisplay();
print_lcd_menu(false);
display.setCursor(0, 8);
display.print(text1);
display.setCursor(0, 24);
display.print(text2);
display.display();
#endif
#ifdef SCREEN_TYPE_1602
lcd.clear();
if (menu == 0) {
lcd.setCursor(0, 0);
lcd.print(text1);
} else {
print_lcd_menu(false);
}
lcd.setCursor(0, 1);
lcd.print(text2);
#endif
#ifdef SCREEN_TYPE_2004
lcd.clear();
print_lcd_menu(false);
lcd.setCursor(0, 1);
lcd.print(text1);
lcd.setCursor(0, 2);
lcd.print(text2);
#endif
}
#ifdef SCREEN_TYPE_SSD1306
void lcd_cll(int line = 1) {
for (int y=0*line; y<=6*line; y++) {
for (int x=0; x<127; x++) {
display.drawPixel(x, y, BLACK);
}
}
}
#endif
String get_text_menu(int p_wpm, int p_tfreq) {
String text_menu = "";
String wpm = (p_wpm == -1) ? " " : String(p_wpm);
String tfreq = (p_tfreq == -1) ? " " : String(p_tfreq);
if (wpm.length() < 2) wpm = " " + wpm;
#ifdef SCREEN_TYPE_SSD1306
text_menu = "WPM: "+wpm+" TONE: "+tfreq;
#endif
#ifdef SCREEN_TYPE_1602
text_menu = "WPM:"+wpm+" TONE:"+tfreq;
#endif
#ifdef SCREEN_TYPE_2004
text_menu = "WPM: "+wpm+" TONE: "+tfreq;
#endif
return text_menu;
}
void print_lcd_menu(boolean cls) {
String text_menu;
if (( menu == 0 ) || ( blink_state == 1 )) {
text_menu = get_text_menu(key_speed, ST_Freq);
}
if (( menu == 1 ) && ( blink_state == 0 )) {
text_menu = get_text_menu(-1, ST_Freq);
}
if (( menu == 2 ) && ( blink_state == 0 )) {
text_menu = get_text_menu(key_speed, -1);
}
if (blink_state == 0) blink_state = 1; else blink_state = 0;
#ifdef SCREEN_TYPE_SSD1306
if ( menu != 0 ) lcd_cll();
display.setCursor(0, 0);
display.print(text_menu);
if (cls) display.display();
#endif
#ifdef SCREEN_TYPE_1602
lcd.setCursor(0, 0);
lcd.print(text_menu);
#endif
#ifdef SCREEN_TYPE_2004
lcd.setCursor(0, 0);
lcd.print(text_menu);
#endif
}