* Speed calculation - Using standard PARIS timing, dot_period(mS) = 1200/WPM
*/
+/*
+ ***************************************************************************************************************
+ * Description of a major overhaul,
+ * Sep/Oct/Nov 2018, by DL1YCF Christoph van Wullen
+ ***************************************************************************************************************
+ *
+ * a) SOME MINOR TECHNICAL ISSUES:
+ * ===============================
+ *
+ * -keyer_close was actually unused. It is now called when local CW is no longer used
+ * and "joins" (terminates) the keyer thread.
+ *
+ * - GPIO pin names are no longer used in iambic.c
+ *
+ * - cw_keyer_spacing can now be set/un-set in the CW menu (cw_menu.c)
+ *
+ * - changing the scheduling policy now becomes only effective for the keyer thread, because
+ * sched_setscheduler() is called at the beginning and the end of the keyer thread with
+ * pid argument 0 (even better: use gettid() return value).
+ * NOTE: this is Linux-specific. Better switch to POSIX calls (e.g. pthread_setschedparam).
+ * mlockall and munlockall still apply process-wide and are therefore executed in
+ * keyer_init/keyer_close.
+ *
+ * b) SIDE TONE GENERATION
+ * =======================
+ *
+ * There is a possibility to generate a side tone on one of the GPIO ports. This is necessary at
+ * speeds greater than 20 wpm since there is a 50 msec delay (due to the LINUX sound system) between
+ * generating the side tone and when it finally appears in the head-phone. The GPIO side tone comes
+ * without a delay.
+ *
+ * The volume of the CW side tone in the standard audio channel is reduced to zero while producing
+ * a square wave on the GPIO pin. The idea is to low-pass this signal and combine it with the audio
+ * output (hardware mixer).
+ *
+ * In the previous version, the side tone was generate using the softTone functionality of wiringPI
+ * within the GPIO module. However, this has a drawback:
+ * one creates two high-priority threads (the tone generator and the keyer)
+ * both firing at high speed (once a milli-sec and faster). This made the frequency of the tone quite
+ * unstable. Instead, the side tone is now explicitly generated within the keyer (if GPIO side tone is activated)
+ * by periodically writing "1" and "0" to the GPIO output while waiting for the end of the just-being-sent
+ * dot or dash. Furthermore, the frequency of the side tone has been stabilized by "sleeping" UNTIL the
+ * pre-calculated output-switching time (and not FOR the calculated amount, this is, using TIMER_ABSTIME
+ * in clock_nanosleep).
+ *
+ * c) CW VOX
+ * =========
+ *
+ * Suppose you hit the paddle while in RX mode. In this case, the SDR automatically switches
+ * to TX, and remains so until a certain time (actually cw_keyer_hang_time, can be set in
+ * the CW menu) has passed since sending the last element (then, PTT is removed).
+ *
+ * - cw_keyer_spacing can now be set/un-set in the CW menu (cw_menu.c)
+ *
+ * - during a dot or dash when no GPIO side tone is produced, the keyer thread simply waits and
+ * does no busy spinning.
+ *
+ * d) DOT/DASH MEMORY
+ * ==================
+ *
+ * For reasons explained below, it is necessary to have TWO such memories for both dot and dash,
+ * they are called dot_memory/dot_held and dash_memory/dash_held. Everything explained here and below
+ * likewise holds for dot/dash exchanged.
+ *
+ * dot_memory is set whenever the dot paddle is hit. The natural way to do this is in the interrupt
+ * service routine. So, keyer_event is the ONLY place where dot_memory is set. When the keyer wants to
+ * know whether the dot paddle has been hit in some interval in time, it has to RESET dot_memory at the
+ * beginning of the interval and can then read it out any time later.
+ *
+ * While the dot paddle may have been hit long ago, it is of interest whether is was still held at
+ * the beginning of the last dash. Therefore, the keyer can store the state of the dot paddle in the
+ * variable dot_held. This is done at the beginning of sending a dash. dot_held is separate from
+ * dot_memory because only dot_held (but not dot_memory) is cleared in iambic mode A when both
+ * paddles are released.
+ *
+ * e) IAMBIC MODES A AND B, AND SINGLE-LEVER PADDLES
+ * =================================================
+ *
+ * It seems that there are lively discussions about what is what, so I distilled out the
+ * following and added one clarification that becomes important when using this mode
+ * with single-lever paddles:
+ *
+ * Suppose you hit the dash paddle and squeeze immediately afterwards (both paddles held),
+ * then the keyer is supposed to produce a "dah dit dah dit dah dit .... " sequence. This is
+ * what "iambic" is all about.
+ *
+ * Now what happens if you release BOTH paddles just after the second "dah" starts to sound?
+ * In Mode-A you produce the letter "K" (dah dit dah), while in Mode-B, you produce the letter
+ * "C" (dah dit dah dit). This is consensus. In this implementation, I follow the plead of
+ * KO0B which I found on the internet and which essentially states that the "time window" for
+ * releasing both paddles should extend until the end of the delay following the second "dah".
+ * This is not only more convenient but also mimics the behaviour of the original Curtis
+ * mode A 8044 chip (KO0B says).
+ *
+ * This means that at the end of the delay following a dash, in Mode-B the condition for
+ * producing a dot is:
+ *
+ * -dot paddle has been held at the beginning of the last dash OR
+ * -dot paddle is just being held OR
+ * -dot paddle has been hit (and possibly already been released) during the time window
+ * from the beginning of the dash until now.
+ *
+ * This implies that in dot_held, the state of the dot key at the beginning of a dash is
+ * stored, and that dot_memory is cleared at the beginning of a dash.
+ *
+ * To implement MODE-A, we clear dot_held at the end of the delay following the dash,
+ * when both keys are released at that point in time.
+ * We do not clear dot_memory in this case! Why?
+ *
+ * All the definitions on mode A/B I found relate to releasing keys that have been
+ * squeezed for some time. Nothing is said about what happens if a key has been
+ * recently hit. I am sometimes using a single-lever paddle, and at higher speeds,
+ * I have also observed that I use a standard (dual-lever) paddle this way, namely
+ * alternatingly hitting the paddle (shortly releasing the dash paddle when hitting
+ * the dot one, and then possibly re-activating the dash paddle).
+ *
+ * It seems consensus that using a single-lever paddle, there should be no difference
+ * between mode-A and mode-B.
+ *
+ * So imagine you want to produce the letter "N" (dah dit) this way. You will hit the dash key,
+ * then immediately afterwards the dot key, and possibly have both keys released
+ * when the final dot of the letter should start. The final dot should not be suppressed
+ * even in mode-A.
+ * Thus: if both keys have been held at the beginning of a dash, and are relased
+ * during the dash or the delay following, this dash is the last element being produced in mode-A.
+ * BUT, if the dot key has been hit during the dash (or the delay following it), then a dot
+ * element will be produced both in mode-A and mode-B, it is not necessary to keep on holding
+ * the dot key.
+ *
+ * The present implementation will make both types happy: those doing pure squeeze and
+ * those releasing one paddle when hitting the other.
+ *
+ **************************************************************************************************************
+ */
+
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/mman.h>
-#include <wiringPi.h>
-
+#ifdef GPIO
#include "gpio.h"
+#endif
#include "radio.h"
#include "new_protocol.h"
#include "iambic.h"
static pthread_t keyer_thread_id;
#define MY_PRIORITY (90)
-#define MAX_SAFE_STACK (8*1024)
#define NSEC_PER_SEC (1000000000)
-/*
-enum {
- CHECK = 0,
- PREDOT,
- PREDASH,
- SENDDOT,
- SENDDASH,
- DOTDELAY,
- DASHDELAY,
- DOTHELD,
- DASHHELD,
- LETTERSPACE,
- EXITLOOP
-};
-*/
-
static int dot_memory = 0;
static int dash_memory = 0;
+static int dot_held = 0;
+static int dash_held = 0;
int key_state = 0;
-static int kdelay = 0;
-static int dot_delay = 0;
-static int dash_delay = 0;
+static int dot_length = 0;
+static int dash_length = 0;
static int kcwl = 0;
static int kcwr = 0;
int *kdot;
int *kdash;
+int *kmemr;
+int *kmeml;
static int running = 0;
#ifdef __APPLE__
static sem_t *cw_event;
__const struct timespec *__req,
struct timespec *__rem);
-void stack_prefault(void) {
- unsigned char dummy[MAX_SAFE_STACK];
-
- memset(dummy, 0, MAX_SAFE_STACK);
- return;
-}
+#ifndef GPIO
+//
+// It makes absolutely no sense to activate LOCALCW but not GPIO.
+// However, if this is done, the code should compile although it does
+// not do anything. Therefore we provide dummy functions related to
+// GPIO side tone generation. Without GPIO, keyer_event is never
+// called so the keyer_thread does nothing.
+//
+int gpio_cw_sidetone_enabled() { return 0; }
+void gpio_cw_sidetone_set(int level) {}
+#endif
void keyer_update() {
- if (!running)
- keyer_init();
-
- dot_delay = 1200 / cw_keyer_speed;
+ //
+ // This function will take notice of changes in the following variables
+ //
+ // cw_keyer_internal
+ // cw_keyer_speed
+ // cw_keyer_weight
+ // cw_keys_reversed
+ //
+ // that might occur asynchronously by changing settings in the CW menu.
+ // Changes to cw_letter_spacing are notices without calling keyer_update.
+ //
+ // The most important thing here is to start/stop the keyer thread.
+ //
+
+ dot_length = 1200 / cw_keyer_speed;
// will be 3 * dot length at standard weight
- dash_delay = (dot_delay * 3 * cw_keyer_weight) / 50;
+ dash_length = (dot_length * 3 * cw_keyer_weight) / 50;
if (cw_keys_reversed) {
- kdot = &kcwr;
+ kdot = &kcwr;
kdash = &kcwl;
+ kmemr = &dot_memory;
+ kmeml = &dash_memory;
} else {
- kdot = &kcwl;
+ kdot = &kcwl;
kdash = &kcwr;
+ kmeml = &dot_memory;
+ kmemr = &dash_memory;
}
+ if (cw_keyer_internal == 0) {
+ if (!running) keyer_init();
+ } else {
+ keyer_close();
+ }
}
-#ifdef GPIO
-void keyer_event(int gpio, int state) {
+//
+// This is called by the paddle interrupt service routine
+//
+// left=1: left paddle triggered event
+// left=0: right paddle triggered event
+//
+// state=0: paddle has been released
+// state=1: paddle has been hit
+//
+void keyer_event(int left, int state) {
if (state) {
- // This is for aborting CAT CW messages if the key is hit.
+ // This is for aborting CAT CW messages if a key is hit.
cw_key_hit = 1;
- // we do PTT as soon as possible, but disable cwvox if
- // PTT has been engaged manually
+ // do PTT already here, unless TX mode was activated before
if (running && !cwvox && !mox) {
g_idle_add(ext_mox_update, (gpointer)(long) 1);
- cwvox=(int) cw_keyer_hang_time;
}
+ cwvox=(int) cw_keyer_hang_time;
}
- if (gpio == CWL_BUTTON)
- kcwl = state;
- //fprintf(stderr,"L=%d\n",state);
- else // CWR_BUTTON
- kcwr = state;
-
- if (state || cw_keyer_mode == KEYER_STRAIGHT) {
+ if (left) {
+ // left paddle hit or released
+ kcwl = state;
+ if (state) *kmeml=1; // trigger dot/dash memory
+ } else {
+ // right paddle hit or released
+ kcwr = state;
+ if (state) *kmemr=1; // trigger dot/dash memory
+ }
+ if (state) {
#ifdef __APPLE__
sem_post(cw_event);
#else
#endif
}
}
-#endif
-
-void clear_memory() {
- dot_memory = 0;
- dash_memory = 0;
-}
void set_keyer_out(int state) {
if (keyer_out != state) {
- // DL1YCF: Shouln't one activate PTT if not yet done?
- // At the moment, it is required to *manually* activate
- // MOX before starting local CW.
keyer_out = state;
if(protocol==NEW_PROTOCOL) schedule_high_priority(9);
- //fprintf(stderr,"set_keyer_out keyer_out= %d\n", keyer_out);
if (state) {
- gpio_sidetone(cw_keyer_sidetone_frequency);
cw_hold_key(1); // this starts a CW pulse in transmitter.c
} else {
- gpio_sidetone(0);
cw_hold_key(0); // this stops a CW pulse in transmitter.c
}
+ } else {
+ // We should not arrive here in a properly designed keyer
+ fprintf(stderr,"SET KEYER OUT: state unchanged: %d", state);
}
}
int pos;
struct timespec loop_delay;
int interval = 1000000; // 1 ms
+ int sidewait;
+ int sideloop;
+ int i;
+ int kdelay;
+ int old_volume;
+ struct sched_param param;
+
+ // In Linux, the new scheduling policy will be effective for the
+ // calling thread only.
+ param.sched_priority = MY_PRIORITY;
+ if(sched_setscheduler((pid_t)0, SCHED_FIFO, ¶m) == -1) {
+ perror("sched_setscheduler failed");
+ }
-fprintf(stderr,"keyer_thread state running= %d\n", running);
+ fprintf(stderr,"keyer_thread state running= %d\n", running);
while(running) {
#ifdef __APPLE__
sem_wait(cw_event);
key_state = CHECK;
- // If MOX still hanging, continue spinnning/checking and decrement cwvox
-
+ clock_gettime(CLOCK_MONOTONIC, &loop_delay);
while (key_state != EXITLOOP || cwvox > 0) {
+ // re-trigger VOX if *not* busy-spinning
if (cwvox > 0 && key_state != EXITLOOP && key_state != CHECK) cwvox=(int) cw_keyer_hang_time;
+
switch (key_state) {
+
case EXITLOOP:
- if (cwvox >0) cwvox--;
+ // If we arrive here, cwvox is greater than zero, since key_state==EXITLOOP
+ // AND cwvox==0 leaves the outer "while" loop.
+ cwvox--;
+ // If CW-vox still hanging, continue "busy-spinning"
if (cwvox == 0) {
+ // we have just reduced cwvox from 1 to 0.
g_idle_add(ext_mox_update,(gpointer)(long) 0);
} else {
key_state=CHECK;
}
break;
+
case CHECK: // check for key press
+ key_state = EXITLOOP; // default next state
+ // Do not decrement cwvox until zero here, otherwise
+ // we won't enter the code 10 lines above that de-activates MOX.
if (cwvox > 1) cwvox--;
if (cw_keyer_mode == KEYER_STRAIGHT) { // Straight/External key or bug
if (*kdash) { // send manual dashes
- set_keyer_out(1);
- key_state = EXITLOOP;
- }
- else if (*kdot) // and automatic dots
- key_state = PREDOT;
- else {
- set_keyer_out(0);
- key_state = EXITLOOP;
+ set_keyer_out(1);
+ clock_gettime(CLOCK_MONOTONIC, &loop_delay);
+ // wait until dash is released
+ if (gpio_cw_sidetone_enabled()) {
+ // produce tone
+ // Note. Using clock_nanosleep with ABSTIME is absolutely
+ // necessary to produce a stable frequency.
+ // You still may painfully recognize that LINUX
+ // e.g. on a RaspberryPi is not a real-time
+ // operating system.
+ // Mute "normal" CW side tone, it will be reactivated
+ // at the end of the following delay.
+ old_volume=cw_keyer_sidetone_volume;
+ cw_keyer_sidetone_volume=0;
+ sidewait=500000000/cw_keyer_sidetone_frequency;
+ for (;;) {
+ gpio_cw_sidetone_set(1);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ gpio_cw_sidetone_set(0);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ if (!*kdash) break;
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ } else {
+ // No-GPIO-sidetone case:
+ // wait until dash is released. Check once a milli-sec
+ for (;;) {
+ loop_delay.tv_nsec += interval;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ if (!*kdash) break;
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ }
+ // dash released.
+ set_keyer_out(0);
+ // wait at least 10ms before re-activating sidetone,
+ // to allow the envelope of the side tone reaching zero
+ if (gpio_cw_sidetone_enabled()) {
+ loop_delay.tv_nsec += 10*interval;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ cw_keyer_sidetone_volume=old_volume;
+ }
}
- }
- else {
- if (*kdot)
- key_state = PREDOT;
- else if (*kdash)
- key_state = PREDASH;
- else {
- set_keyer_out(0);
- key_state = EXITLOOP;
+ if (*kdot) {
+ // "bug" mode: dot key activates automatic dots
+ key_state = SENDDOT;
}
+ // end of KEYER_STRAIGHT case
+ } else {
+ // Paddle
+ // If both following if-statements are true, which one should win?
+ // I think a "simultaneous squeeze" means a dot-dash sequence, since in
+ // a dash-dot sequence there is a larger time window to hit the dot.
+ if (*kdash) key_state = SENDDASH;
+ if (*kdot) key_state = SENDDOT;
}
- break;
- case PREDOT: // need to clear any pending dots or dashes
- clear_memory();
- key_state = SENDDOT;
- break;
- case PREDASH:
- clear_memory();
- key_state = SENDDASH;
- break;
+ break;
- // dot paddle pressed so set keyer_out high for time dependant on speed
- // also check if dash paddle is pressed during this time
- case SENDDOT:
+ case SENDDOT:
+ dash_memory = 0;
+ dash_held = *kdash;
set_keyer_out(1);
- if (kdelay == dot_delay) {
- kdelay = 0;
- set_keyer_out(0);
- key_state = DOTDELAY; // add inter-character spacing of one dot length
- }
- else kdelay++;
-
- // if Mode A and both paddels are relesed then clear dash memory
- if (cw_keyer_mode == KEYER_MODE_A)
- if (!*kdot & !*kdash)
- dash_memory = 0;
- else if (*kdash) // set dash memory
- dash_memory = 1;
- break;
-
- // dash paddle pressed so set keyer_out high for time dependant on 3 x dot delay and weight
- // also check if dot paddle is pressed during this time
- case SENDDASH:
- set_keyer_out(1);
- if (kdelay == dash_delay) {
- kdelay = 0;
- set_keyer_out(0);
- key_state = DASHDELAY; // add inter-character spacing of one dot length
- }
- else kdelay++;
-
- // if Mode A and both padles are relesed then clear dot memory
- if (cw_keyer_mode == KEYER_MODE_A)
- if (!*kdot & !*kdash)
- dot_memory = 0;
- else if (*kdot) // set dot memory
- dot_memory = 1;
+ clock_gettime(CLOCK_MONOTONIC, &loop_delay);
+ if (gpio_cw_sidetone_enabled()) {
+ old_volume=cw_keyer_sidetone_volume;
+ cw_keyer_sidetone_volume=0;
+ sidewait=500000000/cw_keyer_sidetone_frequency;
+ sideloop=(500000*dot_length)/sidewait;
+ for (i=0; i<sideloop; i++) {
+ gpio_cw_sidetone_set(1);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ gpio_cw_sidetone_set(0);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ } else {
+ // No-GPIO-sidetone case: just wait
+ loop_delay.tv_nsec += 1000000 * dot_length;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ set_keyer_out(0);
+ key_state = DOTDELAY; // add inter-character spacing of one dot length
+ kdelay=0;
break;
- // add dot delay at end of the dot and check for dash memory, then check if paddle still held
case DOTDELAY:
- if (kdelay == dot_delay) {
- kdelay = 0;
- if(!*kdot && cw_keyer_mode == KEYER_STRAIGHT) // just return if in bug mode
- key_state = EXITLOOP;
- else if (dash_memory) // dash has been set during the dot so service
- key_state = PREDASH;
- else key_state = DOTHELD; // dot is still active so service
- }
- else kdelay++;
-
- if (*kdash) // set dash memory
- dash_memory = 1;
+ kdelay++;
+ if (kdelay > dot_length) {
+ if (gpio_cw_sidetone_enabled()) {
+ cw_keyer_sidetone_volume=old_volume;
+ }
+ if (cw_keyer_mode == KEYER_STRAIGHT) {
+ // bug mode: continue sending dots or exit, depending on current dot key status
+ key_state = EXITLOOP;
+ if (*kdot) key_state=SENDDOT;
+ // end of bug/straight case
+ } else {
+//
+// DL1YCF:
+// This is my understanding where MODE A comes in:
+// If at the end of the delay, BOTH keys are
+// released, then do not start the next element.
+// However, if the dash has been hit DURING the preceeding
+// dot, produce a dash in either case
+//
+ if (cw_keyer_mode == KEYER_MODE_A && !*kdot && !*kdash) dash_held=0;
+
+ if (dash_memory || *kdash || dash_held)
+ key_state = SENDDASH;
+ else if (*kdot) // dot still held, so send a dot
+ key_state = SENDDOT;
+ else if (cw_keyer_spacing) {
+ dot_memory = dash_memory = 0;
+ key_state = LETTERSPACE;
+ kdelay=0;
+ } else
+ key_state = EXITLOOP;
+ // end of iambic case
+ }
+ }
break;
- // add dot delay at end of the dash and check for dot memory, then check if paddle still held
- case DASHDELAY:
- if (kdelay == dot_delay) {
- kdelay = 0;
-
- if (dot_memory) // dot has been set during the dash so service
- key_state = PREDOT;
- else key_state = DASHHELD; // dash is still active so service
- }
- else kdelay++;
-
- if (*kdot) // set dot memory
- dot_memory = 1;
- break;
-
- // check if dot paddle is still held, if so repeat the dot. Else check if Letter space is required
- case DOTHELD:
- if (*kdot) // dot has been set during the dash so service
- key_state = PREDOT;
- else if (*kdash) // has dash paddle been pressed
- key_state = PREDASH;
- else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes
- clear_memory();
- key_state = LETTERSPACE;
- }
- else key_state = EXITLOOP;
+ case SENDDASH:
+ dot_memory = 0;
+ dot_held = *kdot; // remember if dot is still held at beginning of the dash
+ set_keyer_out(1);
+ clock_gettime(CLOCK_MONOTONIC, &loop_delay);
+ if (gpio_cw_sidetone_enabled()) {
+ old_volume=cw_keyer_sidetone_volume;
+ cw_keyer_sidetone_volume=0;
+ sidewait=500000000/cw_keyer_sidetone_frequency;
+ sideloop=(500000*dash_length)/sidewait;
+ for (i=0; i<sideloop; i++) {
+ gpio_cw_sidetone_set(1);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ gpio_cw_sidetone_set(0);
+ loop_delay.tv_nsec += sidewait;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ } else {
+ // No-GPIO-sidetone case: just wait
+ loop_delay.tv_nsec += 1000000L * dash_length;
+ while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
+ loop_delay.tv_nsec -= NSEC_PER_SEC;
+ loop_delay.tv_sec++;
+ }
+ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
+ }
+ set_keyer_out(0);
+ key_state = DASHDELAY; // add inter-character spacing of one dot length
+ kdelay=0;
+ // do not fall through, update VOX at beginning of loop
break;
- // check if dash paddle is still held, if so repeat the dash. Else check if Letter space is required
- case DASHHELD:
- if (*kdash) // dash has been set during the dot so service
- key_state = PREDASH;
- else if (*kdot) // has dot paddle been pressed
- key_state = PREDOT;
- else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes
- clear_memory();
- key_state = LETTERSPACE;
+ case DASHDELAY:
+ // we never arrive here in STRAIGHT/BUG mode
+ kdelay++;
+ if (kdelay > dot_length) {
+ if (gpio_cw_sidetone_enabled()) {
+ cw_keyer_sidetone_volume=old_volume;
+ }
+//
+// DL1YCF:
+// This is my understanding where MODE A comes in:
+// If at the end of the dash delay, BOTH keys are
+// released, then do not start the next element.
+// However, if the dot has been hit DURING the preceeding
+// dash, produce a dot in either case
+//
+ if (cw_keyer_mode == KEYER_MODE_A && !*kdot && !*kdash) dot_held=0;
+ if (dot_memory || *kdot || dot_held)
+ key_state = SENDDOT;
+ else if (*kdash)
+ key_state = SENDDASH;
+ else if (cw_keyer_spacing) {
+ dot_memory = dash_memory = 0;
+ key_state = LETTERSPACE;
+ kdelay=0;
+ } else key_state = EXITLOOP;
}
- else key_state = EXITLOOP;
break;
- // Add letter space (3 x dot delay) to end of character and check if a paddle is pressed during this time.
- // Actually add 2 x dot_delay since we already have a dot delay at the end of the character.
case LETTERSPACE:
- if (kdelay == 2 * dot_delay) {
- kdelay = 0;
+ // Add letter space (3 x dot delay) to end of character and check if a paddle is pressed during this time.
+ // Actually add 2 x dot_length since we already have a dot delay at the end of the character.
+ kdelay++;
+ if (kdelay > 2 * dot_length) {
if (dot_memory) // check if a dot or dash paddle was pressed during the delay.
- key_state = PREDOT;
+ key_state = SENDDOT;
else if (dash_memory)
- key_state = PREDASH;
+ key_state = SENDDASH;
else key_state = EXITLOOP; // no memories set so restart
}
- else kdelay++;
-
- // save any key presses during the letter space delay
- if (*kdot) dot_memory = 1;
- if (*kdash) dash_memory = 1;
break;
default:
+ fprintf(stderr,"KEYER THREAD: unknown state=%d",(int) key_state);
key_state = EXITLOOP;
-
}
- clock_gettime(CLOCK_MONOTONIC, &loop_delay);
+ // time stamp in loop_delay is either the last time stamp from the
+ // top of the loop, or the last time stamp from tone generation
+ // NOTE: we are using ABSTIME here to produce accurate delays.
loop_delay.tv_nsec += interval;
while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
loop_delay.tv_nsec -= NSEC_PER_SEC;
}
}
-fprintf(stderr,"keyer_thread: EXIT\n");
+ fprintf(stderr,"keyer_thread: EXIT\n");
+ param.sched_priority = 0;
+ sched_setscheduler((pid_t) 0, SCHED_OTHER, ¶m);
}
void keyer_close() {
+ fprintf(stderr,".... closing keyer thread.\n");
running=0;
+ // keyer thread may be sleeping, so wake it up
+#ifdef __APPLE__
+ sem_post(cw_event);
+#else
+ sem_post(&cw_event);
+#endif
+ pthread_join(keyer_thread_id, NULL);
+#ifdef __APPLE__
+ sem_close(cw_event);
+#else
+ sem_close(&cw_event);
+#endif
+ munlockall();
}
int keyer_init() {
int rc;
- struct sched_param param;
-
- param.sched_priority = MY_PRIORITY;
- if(sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
- perror("sched_setscheduler failed");
- running = 0;
- }
+ fprintf(stderr,".... starting keyer thread.\n");
+
if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {
perror("mlockall failed");
- running = 0;
}
+ running = 1;
#ifdef __APPLE__
cw_event=sem_open("CW", O_CREAT, 0700, 0);
rc = (cw_event == SEM_FAILED);
rc = sem_init(&cw_event, 0, 0);
#endif
rc |= pthread_create(&keyer_thread_id, NULL, keyer_thread, NULL);
- running = 1;
if(rc < 0) {
fprintf(stderr,"pthread_create for keyer_thread failed %d\n", rc);
exit(-1);
}
-
return 0;
}
static char cw_buf[30];
static int cw_busy=0;
+static int cat_cw_seen=0;
static long dotlen;
static long dashlen;
//
void send_dash() {
int TimeToGo;
- if (cw_key_hit || cw_not_ready) return;
for(;;) {
TimeToGo=cw_key_up+cw_key_down;
+ // TimeToGo is invalid if local CW keying has set in
+ if (cw_key_hit || cw_not_ready) return;
if (TimeToGo == 0) break;
// sleep until 10 msec before ignition
if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L);
// sleep 1 msec
usleep(1000L);
}
+ // If local CW keying has set in, do not interfere
+ if (cw_key_hit || cw_not_ready) return;
cw_key_down = dashsamples;
cw_key_up = dotsamples;
}
void send_dot() {
int TimeToGo;
- if (cw_key_hit || cw_not_ready) return;
for(;;) {
TimeToGo=cw_key_up+cw_key_down;
+ // TimeToGo is invalid if local CW keying has set in
+ if (cw_key_hit || cw_not_ready) return;
if (TimeToGo == 0) break;
// sleep until 10 msec before ignition
if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L);
// sleep 1 msec
usleep(1000L);
}
+ // If local CW keying has set in, do not interfere
+ if (cw_key_hit || cw_not_ready) return;
cw_key_down = dotsamples;
cw_key_up = dotsamples;
}
void send_space(int len) {
int TimeToGo;
- if (cw_key_hit || cw_not_ready) return;
for(;;) {
TimeToGo=cw_key_up+cw_key_down;
+ // TimeToGo is invalid if local CW keying has set in
+ if (cw_key_hit || cw_not_ready) return;
if (TimeToGo == 0) break;
// sleep until 10 msec before ignition
if (TimeToGo > 500) usleep((long)(TimeToGo-500)*20L);
// sleep 1 msec
usleep(1000L);
}
+ // If local CW keying has set in, do not interfere
+ if (cw_key_hit || cw_not_ready) return;
cw_key_up = len*dotsamples;
}
//
// This thread constantly looks whether CW data
// is available, and produces CW in this case.
-// The buffer is copied to a local buffer and
-// immediately released, such that the next
-// chunk can already be prepeared. This way,
-// splitting a large CW text into words, and
-// sending each word with a separate KY command
-// produces perfectly readable CW.
//
-// UPDATE: we maintain a ring buffer such that
-// the contents of several KY commands
-// can be buffered
+// A ring buffer is maintained such that the contents
+// of several KY commands can be buffered. This allows
+// sending a large CW text word-by-word (each word in a
+// separate KY command).
+//
+// If the contents of the last KY command do not fit into
+// the ring buffer, cw_busy is NOT reset. Eventually, there
+// is enough space in the ring buffer, then cw_busy is reset.
//
static gpointer rigctl_cw_thread(gpointer data)
{
int num_buf=0;
while (server_running) {
- // wait for cw_buf become filled with data
- // (periodically look every 100 msec)
+ // wait for CW data (periodically look every 100 msec)
if (!cw_busy && num_buf ==0) {
cw_key_hit=0;
usleep(100000L);
continue;
}
- // if new data available and space in buffer, copy it
+ // if new data is available and fits into the buffer, copy-in.
// If there are several adjacent spaces, take only the first one.
// This also swallows the "tails" of the KY commands which
// (according to Kenwood) have to be padded with spaces up
num_buf++;
if (write_buf - ring_buf == 128) write_buf=ring_buf; // wrap around
}
- cw_busy=0; // mark buffer free again
+ cw_busy=0; // mark one-line buffer free again
}
// these values may have changed, so recompute them here
CAT_cw_is_active=0;
continue;
}
- // some extra time to settle down, in order NOT to loose
- // the first dit or dah
- usleep(100000L);
}
// At this point, mox==1 and CAT_cw_active == 1
if (cw_key_hit || cw_not_ready) {
//
// CW transmission has been aborted, either due to manually
// removing MOX, changing the mode to non-CW, or because a CW key has been hit.
- // Do not remove PTT if we abort CAT CW because a CW key has been hit.
+ // Do not remove PTT in the latter case
CAT_cw_is_active=0;
- // If an external CW key has been hit, we continue in TX mode
- // doing CW manually. Otherwise, switch PTT off.
- if (!cw_key_hit) {
+ // If a CW key has been hit, we continue in TX mode.
+ // Otherwise, switch PTT off.
+ if (!cw_key_hit && mox) {
g_idle_add(ext_ptt_update ,(gpointer)0);
}
- // Stay for 1 sec swallowing incoming
- // CAT CW commands. We need this long time since
- // hamlib waits 0.5 secs after receiving a "KY1" message before trying to
- // send the next bunch
- cw_busy=-1; // mark buffer purge situation
- usleep(1000000L);
- cw_busy=0;
+ // Let the CAT system swallow incoming CW commands by setting cw_busy to -1.
+ // Do so until no CAT CW message has arrived for 1 second
+ cw_busy=-1;
+ for (;;) {
+ cat_cw_seen=0;
+ usleep(1000000L);
+ if (cat_cw_seen) continue;
+ cw_busy=0;
+ break;
+ }
write_buf=read_buf=ring_buf;
num_buf=0;
} else {
// Otherwise remove PTT and wait for next CAT CW command.
if (cw_busy || num_buf > 0) continue;
CAT_cw_is_active=0;
- g_idle_add(ext_ptt_update ,(gpointer)0);
- // wait up to 500 msec for MOX having gone
- // otherwise there might be a race condition when sending
- // the next character really soon
- i=10;
- while (mox && (i--) > 0) usleep(50000L);
+ if (!cw_key_hit) {
+ g_idle_add(ext_ptt_update ,(gpointer)0);
+ // wait up to 500 msec for MOX having gone
+ // otherwise there might be a race condition when sending
+ // the next character really soon
+ i=10;
+ while (mox && (i--) > 0) usleep(50000L);
+ }
}
+ // end of while (server_running)
}
// We arrive here if the rigctl server shuts down.
// This very rarely happens. But we should shut down the
// of a transmission
rigctl_cw_thread_id = NULL;
cw_busy=0;
- CAT_cw_is_active=0;
- g_idle_add(ext_ptt_update ,(gpointer)0);
+ if (CAT_cw_is_active) {
+ CAT_cw_is_active=0;
+ g_idle_add(ext_ptt_update ,(gpointer)0);
+ }
return NULL;
}
}
else if((strcmp(cmd_str,"KY")==0) && (zzid_flag == 0))
{
+ if (cw_busy < 0) cat_cw_seen=1;
// DL1YCF:
// Hamlib produces timeout errors if we are busy here for
// seconds. Therefore we just move the data into a buffer