MorseTrainer.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /* -*- c -*- */
  2. /* Copyright: (C) 2005 - 2017 by Dipl.-Ing. Stefan Heesch
  3. * callsign: HB9TWS
  4. * email: radio@heesch.net
  5. */
  6. /* Morse drill addition: March 2019
  7. * Chris Baird VK2CJB <cjb@brushtail.apana.org.au>
  8. *
  9. * Usage:
  10. *
  11. * Straight key support only.
  12. * Holding down the key during power-up/reset starts the trainer
  13. * in Stefan's original 'decode only' mode.
  14. * Otherwise, it shows a pseudo-random word from a "100 most common
  15. * words in CW" list I found. Complete it correctly to get another
  16. * one. (And you just have to manage the letters, not the whole word
  17. * perfectly.) Repeat. Repeat. Repeat. Repeat...
  18. *
  19. * To change the testing pool of words, bang in "TEST", and then a
  20. * number --
  21. * 1 = "100 common CW words"
  22. * 2 = Single letters and digits
  23. * 3 = 8-letter uncommon-ish words
  24. * 4 = single letters and digits, with the morse code displayed
  25. * 5 = punctuation, with the morse (what I'm currently studying..)
  26. * 6 = a poem.. (that is some English text)
  27. * 7 = pseudo-callsigns
  28. * 0 = decode only mode
  29. *
  30. * Changing the sidetone is "nnFRQ", where nn is the frequency/100,
  31. * "06FRQ" for example makes it 600 Hz.
  32. *
  33. * "nnWPM" enters 'copy mode', where the trainer beeps out words from
  34. * the current test pool at the given nn-digits speed, and you would
  35. * be doing something like try to write them down. The included poem
  36. * is useful here. Hold down the key to exit.
  37. */
  38. #include <LiquidCrystal.h>
  39. #include <EEPROM.h>
  40. #include "morse.h"
  41. LiquidCrystal lcd(9, 8, 5, 4, 3, 2);
  42. int buzzer = A5;
  43. int morsekey = A4;
  44. int buzzerfreq = 700;
  45. #define KEY_DELAY 15 /* debounce limit, milliseconds */
  46. #define LINE_LENGTH 16
  47. cw m_Signal;
  48. int m_KeyState;
  49. int m_LastKeyState;
  50. char buffer[LINE_LENGTH+1];
  51. char m_Line[LINE_LENGTH+1];
  52. uint8_t m_Position;
  53. char last = ' ';
  54. /* ---------------------------------------------------------------------- */
  55. extern void morse_init (int speed);
  56. extern char morse_decode (cw input);
  57. extern char* morse_failed (void);
  58. extern int morse_speed (void);
  59. extern char morse_check (cw input);
  60. extern int morse_timeout (void);
  61. void newtestword (void);
  62. void flushline (void);
  63. void updatetestline (void);
  64. void show (char s);
  65. void copystring (char* s);
  66. void copymode (void);
  67. void decode ();
  68. int readmorsekey (void);
  69. void setup ();
  70. void loop ();
  71. /* ---------------------------------------------------------------------- */
  72. // Note the trailing spaces in the strings so that they concatenate
  73. const char commontext[] PROGMEM =
  74. "QRL QRM QRN QRQ QRS QRZ QTH QSB QSY "
  75. "TU TNX NAME RST CQ AGN ANT DX ES HI OM PSE PWR WX 73 "
  76. "ABOUT ALL AM AN AND ANY ARE AS AT BE BEEN BEFORE BUT BY CAN COME "
  77. "COULD DO DONT EVERY FIRST FOR FROM GO GREAT HAD HAS HAVE HE HER HIM "
  78. "HIS HOW IF IN INTO IS IT ITS JUST KNOW LIKE LITTLE MADE MAN MAY ME "
  79. "MEAN MEN MORE MUST MY NO NOT NOW OF OH ON ONE ONLY OR OTHER OUR OUT "
  80. "OVER PEOPLE REALLY RIGHT SAID SAY SHALL SHE SHOULD SO SOME SUCH THAN "
  81. "THAT THATS THE THEIR THEM THEN THERE THESE THEY THINK THIS TIME TO "
  82. "TWO UH UP UPON US VERY WAS WE WELL WERE WHAT WHEN WHERE WHICH WHO "
  83. "WILL WITH WORK WOULD YEAH YOU YOUR "
  84. "123 1971 1980 2011 1955 1945 812837 73 "
  85. "BENSBEST WIRE SISTER";
  86. const char longtext[] PROGMEM =
  87. "AFFECTED AFLUKING AHANKARA AMMONIUM BEROIDAE BLAHLAUT "
  88. "CLOWNERY COERCION DEJECTED DICLINIC DIDYMATE ECHOWISE "
  89. "ERGATOID EXCRESCE FASCISTA FJERDING FUNMAKER GEOBLAST "
  90. "HELICOID HERMITRY INERTIAL LENINITE LUTETIAN LYREBIRD "
  91. "MURRELET OROMETRY OUTVALUE OVEREDIT PANORAMA PEYERIAN "
  92. "REEDBUCK SKELPING SUBTOTAL SUITHOLD TOTAQUIN UNCOATED "
  93. "UNHERDED BETATRON BREWSTER CADUCARY DIPHENOL EMPOCKET "
  94. "FEEDBACK FRONDOUS HERODIAN IRONCLAD IZCATECO KAMMALAN "
  95. "NODICORN ORYZOMYS OXYUROUS PORINGLY RACHITIS ROSEMARY "
  96. "SHAVIANA SILICIUM STICKLER STRUMPET SYLPHISH THICKETY "
  97. "TURBINED TYRANNUS UNDERLAY UNHEIRED UNHINTED UPSTROKE";
  98. const char singletext[] PROGMEM =
  99. "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "
  100. "0 1 2 3 4 5 6 7 8 9";
  101. const char singletextcheat[] PROGMEM =
  102. "A_.- B_-... C_-.-. D_-.. E_. F_..-. G_--. H_.... I_.. J_.--- K_-.- "
  103. "L_.-.. M_-- N_-. O_--- P_.--. Q_--.- R_.-. S_... T_- U_..- V_...- "
  104. "W_.-- X_-..- Y_-.-- Z_--.. 0_----- 1_.---- 2_..--- 3_...-- 4_....- "
  105. "5_..... 6_-.... 7_--... 8_---.. 9_----.";
  106. const char symboltext[] PROGMEM =
  107. ",_--..-- ._.-.-.- ?_..--.. /_-..-. =_-...- -_-....- :_---... "
  108. ";_-.-.-. (_-.--. )_-.--.- '_.----. \"_.-..-. @_.--.-.";
  109. const char lawson[] PROGMEM =
  110. "ABOVE THE ASHES STRAIGHT AND TALL THROUGH FERNS WITH MOISTURE DRIPPING "
  111. "I CLIMB BENEATH THE SANDSTONE WALL MY FEET ON MOSSES SLIPPING "
  112. "LIKE RAMPARTS ROUND THE VALLEYS EDGE THE TINTED CLIFFS ARE STANDING "
  113. "WITH MANY A BROKEN WALL AND LEDGE AND MANY A ROCKY LANDING "
  114. "AND ROUND ABOUT THEIR RUGGED FEET DEEP FERNY DELLS ARE HIDDEN "
  115. "IN SHADOWED DEPTHS WHENCE DUST AND HEAT ARE BANISHED AND FORBIDDEN "
  116. "THE STREAM THAT CROONING TO ITSELF COMES DOWN A TIRELESS ROVER "
  117. "FLOWS CALMLY TO THE ROCKY SHELF AND THERE LEAPS BRAVELY OVER "
  118. "NOW POURING DOWN NOW LOST IN SPRAY WHEN MOUNTAIN BREEZES SALLY "
  119. "THE WATER STRIKES THE ROCK MIDWAY AND LEAPS INTO THE VALLEY "
  120. "NOW IN THE WEST THE COLOURS CHANGE THE BLUE WITH CRIMSON BLENDING "
  121. "BEHIND THE FAR DIVIDING RANGE THE SUN IS FAST DESCEND "
  122. "AND MELLOWED DAY COMES OVER THE PLACE AND SOFTENS RAGGED EDGES "
  123. "THE RISING MOONS GREAT PLACID FACE LOOKS GRAVELY OVER THE LEDGES "
  124. "= BLUE MOUNTAINS BY HENRY LAWSON 1888.";
  125. const char callsigntemplate[] PROGMEM =
  126. "@n@@ @n@@@ n@n@@@ n@@n@@ @@n@@ @@n@@@ @@nn@@ "
  127. "VKn@@@ VKn@@ VKnF@@@ ZLn@@ ZLn@@@";
  128. const char* const texts[] PROGMEM =
  129. {
  130. 0, commontext, singletext, longtext, singletextcheat, symboltext,
  131. lawson, callsigntemplate
  132. };
  133. #define numtexts (sizeof(texts)/sizeof(char*))
  134. char testword[12];
  135. char testpnt;
  136. unsigned char testmode = 1;
  137. /* ---------------------------------------------------------------------- */
  138. /* pick a ~random~ word in a space-delimited string */
  139. unsigned int lp = 0;
  140. void newtestword (void)
  141. {
  142. unsigned int f = pgm_read_word(&texts[testmode]);
  143. int i = 1;
  144. if (testmode != 6) /* make the poem go sequentially */
  145. i += millis() & 63;
  146. for (; i; i--)
  147. {
  148. char c;
  149. int j = 0;
  150. while ((c = pgm_read_byte(f+lp)) > ' ')
  151. {
  152. testword[j++] = c;
  153. lp = (c ? lp+1 : 0);
  154. }
  155. testword[j] = 0;
  156. lp = (c == ' ' ? lp+1 : 0);
  157. }
  158. if (testword[1] == '_') testword[1] = 32;
  159. if (testmode == 7)
  160. {
  161. randomSeed (millis());
  162. for (i = 0; testword[i]; i++)
  163. {
  164. if (testword[i] == '@')
  165. do testword[i] = 65 + (random() % 26);
  166. while (testword[i] == 'Q'); /* most countries avoid it */
  167. if (testword[i] == 'n')
  168. testword[i] = 48 + (random() % 10);
  169. }
  170. }
  171. testpnt = 0;
  172. }
  173. /* ---------------------------------------------------------------------- */
  174. void flushline (void)
  175. {
  176. for (int i = 0; i < LINE_LENGTH-1; i++)
  177. m_Line[i] = ' ';
  178. m_Position = LINE_LENGTH-1;
  179. }
  180. /* ---------------------------------------------------------------------- */
  181. void updatetestline (void)
  182. {
  183. if (testmode)
  184. {
  185. int p;
  186. lcd.setCursor(0, 0);
  187. for (p = 0; testword[p]; p++)
  188. lcd.write((char)testword[p]);
  189. while (p++ < (LINE_LENGTH-6))
  190. lcd.write(' ');
  191. lcd.cursor();
  192. lcd.setCursor(testpnt, 0);
  193. }
  194. else
  195. lcd.noCursor();
  196. }
  197. /* ---------------------------------------------------------------------- */
  198. void show (char s)
  199. {
  200. m_Line[m_Position] = s;
  201. if (++m_Position > LINE_LENGTH-1)
  202. {
  203. for (int i = 0; i < LINE_LENGTH-1; i++)
  204. m_Line[i] = m_Line[i + 1];
  205. m_Line[LINE_LENGTH-1] = ' ';
  206. m_Position = LINE_LENGTH-1;
  207. }
  208. lcd.setCursor(0, 1);
  209. lcd.print(m_Line);
  210. updatetestline();
  211. }
  212. /* ---------------------------------------------------------------------- */
  213. int copywpm = 10;
  214. /* Idea from 'Efficient Storage Of Morse Code Character Codes',
  215. * Lawrence Krakauer, page 36, Issue 14, BYTE 1976-10 */
  216. const unsigned char morse_table[] PROGMEM =
  217. {
  218. 'A', 0b110, 'B', 0b10001, 'C', 0b10101, 'D', 0b1001,
  219. 'E', 0b10, 'F', 0b10100, 'G', 0b1011, 'H', 0b10000,
  220. 'I', 0b100, 'J', 0b11110, 'K', 0b1101, 'L', 0b10010,
  221. 'M', 0b111, 'N', 0b101, 'O', 0b1111, 'P', 0b10110,
  222. 'Q', 0b11011, 'R', 0b1010, 'S', 0b1000, 'T', 0b11,
  223. 'U', 0b1100, 'V', 0b11000, 'W', 0b1110, 'X', 0b11001,
  224. 'Y', 0b11101, 'Z', 0b10011, '0', 0b111111, '1', 0b111110,
  225. '2', 0b111100, '3', 0b111000, '4', 0b110000, '5', 0b100000,
  226. '6', 0b100001, '7', 0b100011, '8', 0b100111, '9', 0b101111,
  227. ',', 0b1110011,'.', 0b1101010,'?', 0b1001100,'/', 0b101001,
  228. '=', 0b110001, '-', 0b1100001,':', 0b1000111,';', 0b1010101,
  229. '(', 0b101101, ')', 0b1101101,'\'',0b1011110,'"', 0b1010010,
  230. '@', 0b1010110,
  231. 0, 0
  232. };
  233. void copystring (char* s)
  234. {
  235. int dit = 2000 / copywpm;
  236. while (*s)
  237. {
  238. int i;
  239. if (*s == '_') break;
  240. for (i = 0; pgm_read_byte(&morse_table[i]); i += 2)
  241. if (pgm_read_byte(&morse_table[i]) == *s) break;
  242. if (pgm_read_byte(&morse_table[i]))
  243. {
  244. unsigned char b = pgm_read_byte(&morse_table[i+1]);
  245. while (b != 1)
  246. {
  247. tone (buzzer, buzzerfreq);
  248. if (b & 1) delay (3*dit); /* do dash (3 dits) */
  249. else delay (dit); /* do dit (1 dit) */
  250. noTone (buzzer);
  251. delay(dit); /* do inter-symbol delay (1 dit) */
  252. b >>= 1;
  253. }
  254. }
  255. else
  256. delay (5*dit); /* assume it's a space (7-2 dits) */
  257. delay (2*dit); /* do intra-letter space (3-1 dits) */
  258. s++;
  259. }
  260. }
  261. void copymode (void)
  262. {
  263. do
  264. {
  265. lcd.clear();
  266. flushline();
  267. lcd.noCursor();
  268. lcd.print(F("COPY.KEY TO EXIT"));
  269. newtestword();
  270. copystring(testword);
  271. for (int i = 0; testword[i]; i++)
  272. {
  273. if (readmorsekey()) break;
  274. delay(750);
  275. }
  276. }
  277. while (readmorsekey()== 0);
  278. }
  279. /* ---------------------------------------------------------------------- */
  280. void decode()
  281. {
  282. static char cmdbuf[5];
  283. cw input;
  284. input.level = m_Signal.level;
  285. input.duration = m_Signal.duration;
  286. char s = morse_decode(input);
  287. if (s > 0)
  288. {
  289. last = s;
  290. switch (s)
  291. {
  292. case 'a': show('a'); show('r'); break;
  293. case 'k': show('k'); show('a'); break;
  294. case 's': show('s'); show('k'); break;
  295. case DECODING_ERROR: /*show('_');*/ break;
  296. default: show(s); break;
  297. }
  298. if (s != ' ')
  299. {
  300. lcd.setCursor(0, 0);
  301. if (s == DECODING_ERROR)
  302. {
  303. for (char *p = morse_failed(); *p; p++)
  304. show(*p);
  305. }
  306. else
  307. {
  308. sprintf(buffer, " %2d wpm", morse_speed() / 5);
  309. lcd.print(buffer);
  310. updatetestline();
  311. }
  312. }
  313. /* command trigger */
  314. for (int i = 4; i; i--) cmdbuf[i] = cmdbuf[i-1];
  315. cmdbuf[0] = s;
  316. if ((cmdbuf[4] == 'T') && (cmdbuf[3] == 'E') && (cmdbuf[2] == 'S') &&
  317. (cmdbuf[1] == 'T'))
  318. {
  319. if ((s > 47) && (s < (48+numtexts)))
  320. {
  321. delay (50);
  322. testmode = s - 48;
  323. updateeeprom();
  324. lp = 0;
  325. newtestword();
  326. tone (buzzer, 800); delay (100);
  327. tone (buzzer, 1600); delay (100);
  328. noTone(buzzer);
  329. lcd.clear();
  330. if (testmode == 0) lcd.print (F("DECODE MODE"));
  331. flushline();
  332. }
  333. }
  334. /* copy mode */
  335. if ((cmdbuf[2] == 'W') && (cmdbuf[1] == 'P') && (cmdbuf[0] == 'M'))
  336. {
  337. copywpm = (cmdbuf[4]*10) + cmdbuf[3] - 480 - 48;
  338. if ((copywpm > 4) && (copywpm < 41))
  339. copymode();
  340. }
  341. /* change sidetone */
  342. if ((cmdbuf[2] == 'F') && (cmdbuf[1] == 'R') && (cmdbuf[0] == 'Q'))
  343. {
  344. int i = (cmdbuf[4]*10) + cmdbuf[3] - 480 - 48;
  345. if ((i > 1) && (i < 30))
  346. {
  347. delay (50);
  348. tone (buzzer, 1000); delay (50);
  349. tone (buzzer, 900); delay (50);
  350. noTone(buzzer);
  351. buzzerfreq = i * 100;
  352. updateeeprom();
  353. }
  354. }
  355. /* jym joke */
  356. if ((cmdbuf[4] == 'R') && (cmdbuf[3] == 'U') && (cmdbuf[2] == 'O') &&
  357. (cmdbuf[1] == 'K') && (cmdbuf[0] == '?'))
  358. {
  359. copywpm = morse_speed() / 5;
  360. copystring ("HUG ME");
  361. }
  362. /* completed */
  363. if ((testmode) && (s == testword[testpnt]))
  364. {
  365. testpnt++;
  366. if (testword[testpnt] < 33)
  367. {
  368. delay (50);
  369. tone (buzzer, 1200); delay (50);
  370. tone (buzzer, 1600); delay (50);
  371. noTone(buzzer);
  372. newtestword();
  373. flushline();
  374. }
  375. }
  376. }
  377. /* Update speed and check for new word */
  378. s = morse_check(input);
  379. if (s == ' ' && last != ' ' && m_Position > 0)
  380. {
  381. last = s;
  382. show(s);
  383. }
  384. }
  385. /* ---------------------------------------------------------------------- */
  386. int readmorsekey (void)
  387. {
  388. return digitalRead(morsekey) > 0 ? 0 : 1;
  389. }
  390. /* ---------------------------------------------------------------------- */
  391. void updateeeprom (void)
  392. {
  393. EEPROM.write(0,testmode);
  394. EEPROM.write(1, buzzerfreq >> 8);
  395. EEPROM.write(2, buzzerfreq & 255);
  396. }
  397. void setup()
  398. {
  399. testmode = EEPROM.read(0);
  400. buzzerfreq = (EEPROM.read(1) << 8) + EEPROM.read(2);
  401. if (testmode > numtexts) testmode = 1;
  402. if ((buzzerfreq < 100) || (buzzerfreq > 3000)) buzzerfreq = 700;
  403. for (int i = 0; i<LINE_LENGTH-1; i++)
  404. {
  405. buffer[i] = 0;
  406. m_Line[i] = ' ';
  407. }
  408. buffer[LINE_LENGTH - 1] = 0;
  409. m_Line[LINE_LENGTH - 1] = 0;
  410. flushline();
  411. digitalWrite(morsekey, HIGH);
  412. pinMode(morsekey, INPUT_PULLUP);
  413. if (readmorsekey())
  414. testmode = 0; /* key down -> start with original decoder */
  415. lcd.begin(LINE_LENGTH, 2);
  416. lcd.setCursor(0, testmode ? 0 : 1);
  417. lcd.print(F(" Morse Trainer"));
  418. lcd.setCursor(0, testmode ? 1 : 0);
  419. lcd.print(F("HB9TWS & VK2CJB"));
  420. tone(buzzer, 324); delay(300);
  421. tone(buzzer, 648); delay(300);
  422. tone(buzzer, 1292); delay(300);
  423. noTone(buzzer); delay(1500);
  424. morse_init(60);
  425. while (readmorsekey()) continue; /* key up before continuing */
  426. m_KeyState = 0;
  427. m_LastKeyState = 0;
  428. m_Signal.level = 0;
  429. m_Signal.duration = 0;
  430. updateeeprom();
  431. lcd.clear();
  432. newtestword();
  433. updatetestline();
  434. if (testmode == 0) lcd.print (F("DECODE MODE"));
  435. }
  436. /* ---------------------------------------------------------------------- */
  437. void loop()
  438. {
  439. static unsigned long m_Start = 0;
  440. static unsigned long m_DebounceTime;
  441. unsigned long now = millis();
  442. if (m_Start == 0)
  443. {
  444. m_Start = now;
  445. m_DebounceTime = now;
  446. return;
  447. }
  448. m_KeyState = readmorsekey();
  449. if (m_KeyState != m_LastKeyState)
  450. m_DebounceTime = millis();
  451. if (now - m_DebounceTime > KEY_DELAY)
  452. {
  453. if (m_Signal.level != m_KeyState)
  454. {
  455. m_Signal.duration = now - m_Start;
  456. decode();
  457. m_Signal.level = m_KeyState;
  458. m_Start = now;
  459. }
  460. if (now - m_Start > morse_timeout() && m_Signal.level == 0)
  461. {
  462. m_Signal.duration = now - m_Start;
  463. decode();
  464. m_Start = now;
  465. }
  466. }
  467. /* side tone */
  468. m_LastKeyState = m_KeyState;
  469. if (m_KeyState == 1)
  470. tone(buzzer, buzzerfreq);
  471. else
  472. noTone(buzzer);
  473. }
  474. /* ---------------------------------------------------------------------- */