aboutsummaryrefslogtreecommitdiffstats
path: root/src/common/Core/fsm.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/common/Core/fsm.c')
-rw-r--r--src/common/Core/fsm.c679
1 files changed, 679 insertions, 0 deletions
diff --git a/src/common/Core/fsm.c b/src/common/Core/fsm.c
new file mode 100644
index 0000000..9fe2c4c
--- /dev/null
+++ b/src/common/Core/fsm.c
@@ -0,0 +1,679 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2018 Matthias P. Braendli, Maximilien Cuony
+ *
+ * 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 <string.h>
+#include <stdio.h>
+#include <stdint.h>
+#include "Core/common.h"
+#include "Core/fsm.h"
+#include "GPIO/usart.h"
+#include "GPIO/temperature.h"
+#include "GPIO/analog.h"
+
+static struct fsm_input_signals_t fsm_in;
+static struct fsm_output_signals_t fsm_out;
+
+static fsm_state_t current_state;
+static balise_fsm_state_t balise_state;
+static sstv_fsm_state_t sstv_state;
+
+// Keep track of when we last entered a given state, measured
+// in ms using the timestamp_now() function
+static uint64_t timestamp_state[_NUM_FSM_STATES];
+
+static int last_supply_voltage_decivolts = 0;
+
+#define CW_MESSAGE_BALISE_LEN 64
+static char cw_message_balise[CW_MESSAGE_BALISE_LEN];
+
+
+// Each 20 minutes, send a SHORT_BEACON
+#define SHORT_BEACON_MAX (60 * 20)
+// Reset the counter if the QSO was 10m too long
+#define SHORT_BEACON_RESET_IF_QSO (60 * 10)
+
+/* At least 1 second predelay for CW, ensures the receivers had enough time
+ * time to open their squelch before the first letter gets transmitted
+ */
+#define CW_PREDELAY " "
+
+// Some time to ensure we don't cut off the last letter
+#define CW_POSTDELAY " "
+
+// The counter (up to 20 minutes) for the short balise
+static int short_beacon_counter_s = 0;
+static uint64_t short_beacon_counter_last_update = 0;
+
+// The last start of the last qso
+static uint64_t last_qso_start_timestamp = 0;
+
+// Information from which we can calculate the QSO duration
+static struct {
+ int qso_occurred;
+ uint64_t qso_start_time;
+} qso_info;
+
+void fsm_init() {
+ memset(&fsm_in, 0, sizeof(fsm_in));
+ memset(&fsm_out, 0, sizeof(fsm_out));
+
+ memset(timestamp_state, 0, _NUM_FSM_STATES * sizeof(*timestamp_state));
+ timestamp_state[FSM_OISIF] = timestamp_now();
+
+ current_state = FSM_OISIF;
+ balise_state = BALISE_FSM_EVEN_HOUR;
+ sstv_state = SSTV_FSM_OFF;
+
+ qso_info.qso_occurred = 0;
+ qso_info.qso_start_time = timestamp_now();
+}
+
+// Calculate the time spent in the current state
+static uint64_t fsm_current_state_time_ms(void) {
+ return timestamp_now() - timestamp_state[current_state];
+}
+
+static uint64_t fsm_current_state_time_s(void) {
+ return fsm_current_state_time_ms() / 1000;
+}
+
+static const char* state_name(fsm_state_t state) {
+ switch (state) {
+ case FSM_OISIF: return "FSM_OISIF";
+ case FSM_OPEN1: return "FSM_OPEN1";
+ case FSM_OPEN2: return "FSM_OPEN2";
+ case FSM_LETTRE: return "FSM_LETTRE";
+ case FSM_ECOUTE: return "FSM_ECOUTE";
+ case FSM_ATTENTE: return "FSM_ATTENTE";
+ case FSM_QSO: return "FSM_QSO";
+ case FSM_ANTI_BAVARD: return "FSM_ANTI_BAVARD";
+ case FSM_BLOQUE: return "FSM_BLOQUE";
+ case FSM_TEXTE_73: return "FSM_TEXTE_73";
+ case FSM_TEXTE_HB9G: return "FSM_TEXTE_HB9G";
+ case FSM_TEXTE_LONG: return "FSM_TEXTE_LONG";
+ case FSM_BALISE_LONGUE: return "FSM_BALISE_LONGUE";
+ case FSM_BALISE_SPECIALE: return "FSM_BALISE_SPECIALE";
+ case FSM_BALISE_COURTE: return "FSM_BALISE_COURTE";
+ case FSM_BALISE_COURTE_OPEN: return "FSM_BALISE_COURTE_OPEN";
+ default: return "ERROR!";
+ }
+}
+
+static const char* balise_state_name(balise_fsm_state_t state) {
+ switch (state) {
+ case BALISE_FSM_EVEN_HOUR: return "BALISE_FSM_EVEN_HOUR";
+ case BALISE_FSM_ODD_HOUR: return "BALISE_FSM_ODD_HOUR";
+ case BALISE_FSM_PENDING: return "BALISE_FSM_PENDING";
+ default: return "ERROR!";
+ }
+}
+
+static const char* sstv_state_name(sstv_fsm_state_t state) {
+ switch (state) {
+ case SSTV_FSM_OFF: return "SSTV_FSM_OFF";
+ case SSTV_FSM_ON: return "SSTV_FSM_ON";
+ default: return "ERROR!";
+ }
+}
+
+static fsm_state_t select_grande_balise(void) {
+ if (fsm_in.qrp || fsm_in.swr_high) {
+ return FSM_BALISE_SPECIALE;
+ }
+ else {
+ return FSM_BALISE_LONGUE;
+ }
+}
+
+static uint64_t qso_duration(void) {
+ return timestamp_state[current_state] - qso_info.qso_start_time;
+}
+
+// Between turns in a QSO, the repeater sends a letter in CW,
+// different messages are possible. They are sorted here from
+// low to high priority.
+const char* letter_all_ok = "K";
+const char* letter_sstv = "S";
+const char* letter_qrp = "G";
+const char* letter_freq_high = "U";
+const char* letter_freq_low = "D";
+const char* letter_swr_high = "R";
+
+static const char* fsm_select_letter(void) {
+ if (fsm_in.swr_high) {
+ return letter_swr_high;
+ }
+ else if (fsm_in.discrim_d) {
+ return letter_freq_low;
+ }
+ else if (fsm_in.discrim_u) {
+ return letter_freq_high;
+ }
+ else if (fsm_in.qrp) {
+ return letter_qrp;
+ }
+ else if (sstv_state == SSTV_FSM_ON) {
+ return letter_sstv;
+ }
+
+ return letter_all_ok;
+}
+
+
+void fsm_update() {
+
+ fsm_state_t next_state = current_state;
+
+ // Some defaults for the outgoing signals
+ fsm_out.tx_on = 0;
+ fsm_out.modulation = 0;
+ fsm_out.cw_psk31_trigger = 0;
+ fsm_out.cw_dit_duration = 50;
+ fsm_out.msg_frequency = 960;
+ fsm_out.require_tone_detector = 0;
+ // other output signals keep their value
+
+ switch (current_state) {
+ case FSM_OISIF:
+ // Check the length of the last QSO, and reset the SHORT_BEACON counter if needed
+ if (last_qso_start_timestamp != 0) {
+
+ if ((timestamp_now() - last_qso_start_timestamp) > 1000 * SHORT_BEACON_RESET_IF_QSO) {
+ short_beacon_counter_s = 0;
+ }
+
+ last_qso_start_timestamp = 0;
+ }
+
+ // Increment the SHORT_BEACON counter based on time spent in the state
+ while(short_beacon_counter_s < SHORT_BEACON_MAX && (fsm_current_state_time_s() - short_beacon_counter_last_update > 1)) {
+ short_beacon_counter_last_update++;
+ short_beacon_counter_s++;
+ }
+
+ // SQ and button 1750 are debounced inside pio.c (300ms)
+ fsm_out.require_tone_detector = fsm_in.sq;
+
+ if ( (fsm_in.sq && fsm_in.det_1750) ||
+ (fsm_in.sq && sstv_state == SSTV_FSM_ON) ||
+ (fsm_in.button_1750)) {
+ next_state = FSM_OPEN1;
+ }
+ else if (balise_state == BALISE_FSM_PENDING) {
+ short_beacon_counter_s = 0;
+ next_state = select_grande_balise();
+ }
+ else if (!fsm_in.qrp && short_beacon_counter_s == SHORT_BEACON_MAX) {
+ short_beacon_counter_s = 0;
+ next_state = FSM_BALISE_COURTE;
+ }
+
+ break;
+
+ case FSM_OPEN1:
+ /* Do not enable TX_ON here, otherwise we could get stuck transmitting
+ * forever if SQ never goes low.
+ */
+ fsm_out.require_tone_detector = 1;
+ if (!fsm_in.sq && !fsm_in.det_1750) {
+ next_state = FSM_OPEN2;
+ }
+ break;
+
+ case FSM_OPEN2:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+ qso_info.qso_occurred = 0;
+ qso_info.qso_start_time = timestamp_now();
+
+ if (fsm_current_state_time_ms() > 200) {
+ next_state = FSM_LETTRE;
+ }
+ break;
+
+ case FSM_LETTRE:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+ fsm_out.msg = fsm_select_letter();
+ if (fsm_out.msg[0] == 'G') {
+ // The letter 'G' is a bit different
+ fsm_out.msg_frequency = 696;
+ }
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (fsm_in.cw_psk31_done) {
+ next_state = FSM_ECOUTE;
+ }
+ break;
+
+ case FSM_ECOUTE:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+
+ /* Time checks:
+ * We need to check the total TX_ON duration to decide the text to
+ * send. This is the QSO duration.
+ *
+ * We also need to check if we actually entered the QSO state
+ * recently, otherwise we want to go to ATTENTE. That's why the
+ * additional field qso_occurred is required.
+ */
+
+ if (fsm_in.sq) {
+ next_state = FSM_QSO;
+ }
+ else {
+ if (fsm_current_state_time_s() > 5) {
+ if (balise_state == BALISE_FSM_PENDING) {
+ short_beacon_counter_s = 0;
+ next_state = select_grande_balise();
+ }
+ else if (qso_info.qso_occurred) {
+ if (qso_duration() >= 1000ul * 15 * 60) {
+ next_state = FSM_TEXTE_LONG;
+ }
+ else if (qso_duration() >= 1000ul * 10 * 60) {
+ next_state = FSM_TEXTE_HB9G;
+ }
+ else if (qso_duration() >= 1000ul * 5 * 60) {
+ next_state = FSM_TEXTE_73;
+ }
+ else {
+ next_state = FSM_OISIF;
+ }
+ }
+ }
+
+ if (fsm_current_state_time_s() > 6 && !qso_info.qso_occurred) {
+ next_state = FSM_ATTENTE;
+ }
+
+ /* If everything fails and the state was not changed after 7
+ * seconds, fall back to oisif
+ */
+ if (fsm_current_state_time_s() > 7) {
+ next_state = FSM_OISIF;
+ }
+ }
+ break;
+
+ case FSM_ATTENTE:
+ if (fsm_in.sq) {
+ fsm_out.require_tone_detector = 1;
+ next_state = FSM_ECOUTE;
+ }
+ else if (fsm_current_state_time_s() > 15) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_QSO:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+ qso_info.qso_occurred = 1;
+
+ // Save the starting timestamp, if there is none
+ if (last_qso_start_timestamp == 0) {
+ last_qso_start_timestamp = timestamp_now();
+ }
+
+ if (!fsm_in.sq && fsm_current_state_time_s() < 3) {
+ /* To avoid that very short open squelch triggers
+ * transmit CW letters all the time. Some people
+ * enjoy doing that.
+ */
+ next_state = FSM_ECOUTE;
+ }
+ else if (!fsm_in.sq && fsm_current_state_time_s() >= 3) {
+ next_state = FSM_LETTRE;
+ }
+ else if (fsm_current_state_time_s() > 5 * 60) {
+ next_state = FSM_ANTI_BAVARD;
+ }
+ break;
+
+ case FSM_ANTI_BAVARD:
+ fsm_out.tx_on = 1;
+ // No modulation!
+
+ // Short post-delay to underscore the fact that
+ // transmission was forcefully cut off.
+ fsm_out.msg = " HI HI ";
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (fsm_in.cw_psk31_done) {
+ next_state = FSM_BLOQUE;
+ }
+ break;
+
+ case FSM_BLOQUE:
+ if (fsm_current_state_time_s() > 10) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_TEXTE_73:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+ fsm_out.msg_frequency = 696;
+ fsm_out.cw_dit_duration = 70;
+ fsm_out.msg = " 73" CW_POSTDELAY;
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (fsm_in.sq) {
+ next_state = FSM_QSO;
+ qso_info.qso_start_time = timestamp_now();
+ }
+ else if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_TEXTE_HB9G:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+ fsm_out.msg_frequency = 696;
+ fsm_out.cw_dit_duration = 70;
+ // No need for CW_PREDELAY, since we are already transmitting
+ fsm_out.msg = " HB9G" CW_POSTDELAY;
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (fsm_in.sq) {
+ next_state = FSM_QSO;
+ qso_info.qso_start_time = timestamp_now();
+ }
+ else if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_TEXTE_LONG:
+ fsm_out.tx_on = 1;
+ fsm_out.modulation = 1;
+ fsm_out.require_tone_detector = 1;
+
+ fsm_out.msg_frequency = 696;
+ fsm_out.cw_dit_duration = 70;
+
+ // No need for CW_PREDELAY, since we are already transmitting
+ if (random_bool()) {
+ fsm_out.msg = " HB9G 1628M" CW_POSTDELAY;
+ }
+ else {
+ fsm_out.msg = " HB9G JN36BK" CW_POSTDELAY;
+ }
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (fsm_in.sq) {
+ next_state = FSM_QSO;
+ qso_info.qso_start_time = timestamp_now();
+ }
+ else if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_BALISE_LONGUE:
+ fsm_out.tx_on = 1;
+ fsm_out.msg_frequency = 588;
+ fsm_out.cw_dit_duration = 110;
+
+ {
+ const float supply_voltage = round_float_to_half_steps(analog_measure_12v());
+ const int supply_decivolts = supply_voltage * 10.0f;
+
+ char *eol_info = "73";
+ if (!fsm_in.wind_generator_ok) {
+ eol_info = "\\";
+ // The backslash is the SK digraph
+ }
+
+ char supply_trend = '=';
+ // = means same voltage as previous
+ // + means higher
+ // - means lower
+ if (last_supply_voltage_decivolts < supply_decivolts) {
+ supply_trend = '+';
+ }
+ else if (last_supply_voltage_decivolts > supply_decivolts) {
+ supply_trend = '-';
+ }
+
+ if (temperature_valid()) {
+ snprintf(cw_message_balise, CW_MESSAGE_BALISE_LEN-1,
+ CW_PREDELAY "HB9G JN36BK 1628M U %dV%01d %c T %d %s" CW_POSTDELAY,
+ supply_decivolts / 10,
+ supply_decivolts % 10,
+ supply_trend,
+ (int)(round_float_to_half_steps(temperature_get())),
+ eol_info);
+ }
+ else {
+ snprintf(cw_message_balise, CW_MESSAGE_BALISE_LEN-1,
+ CW_PREDELAY "HB9G JN36BK 1628M U %dV%01d %c %s" CW_POSTDELAY,
+ supply_decivolts / 10,
+ supply_decivolts % 10,
+ supply_trend,
+ eol_info);
+ }
+
+ fsm_out.msg = cw_message_balise;
+
+ last_supply_voltage_decivolts = supply_decivolts;
+
+ fsm_out.cw_psk31_trigger = 1;
+ }
+
+ if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_BALISE_SPECIALE:
+ fsm_out.tx_on = 1;
+ fsm_out.msg_frequency = 696;
+ fsm_out.cw_dit_duration = 70;
+
+ {
+ const float supply_voltage = round_float_to_half_steps(analog_measure_12v());
+ const int supply_decivolts = supply_voltage * 10.0f;
+
+ char *eol_info = "73";
+ if (!fsm_in.wind_generator_ok) {
+ eol_info = "\\";
+ // The backslash is the SK digraph
+ }
+
+ snprintf(cw_message_balise, CW_MESSAGE_BALISE_LEN-1,
+ CW_PREDELAY "HB9G U %dV%01d %s" CW_POSTDELAY,
+ supply_decivolts / 10,
+ supply_decivolts % 10,
+ eol_info);
+
+ fsm_out.msg = cw_message_balise;
+
+ fsm_out.cw_psk31_trigger = 1;
+ }
+
+ if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OISIF;
+ }
+ break;
+
+ case FSM_BALISE_COURTE:
+ case FSM_BALISE_COURTE_OPEN:
+
+ fsm_out.tx_on = 1;
+
+ fsm_out.msg_frequency = 696;
+ fsm_out.cw_dit_duration = 70;
+
+ {
+ int rand = random_bool() * 2 + random_bool();
+
+ if (rand == 0) {
+ fsm_out.msg = CW_PREDELAY "HB9G" CW_POSTDELAY;
+ }
+ else if (rand == 1) {
+ fsm_out.msg = CW_PREDELAY "HB9G JN36BK" CW_POSTDELAY;
+ }
+ else if (rand == 2) {
+ fsm_out.msg = CW_PREDELAY "HB9G 1628M" CW_POSTDELAY;
+ }
+ else {
+ fsm_out.msg = CW_PREDELAY "HB9G JN36BK 1628M" CW_POSTDELAY;
+ }
+ }
+ fsm_out.cw_psk31_trigger = 1;
+
+ if (current_state == FSM_BALISE_COURTE) {
+ if (fsm_in.cw_psk31_done) {
+ if (fsm_in.sq) {
+ next_state = FSM_OPEN2;
+ }
+ else {
+ next_state = FSM_OISIF;
+ }
+ }
+ else if (fsm_in.sq) {
+ next_state = FSM_BALISE_COURTE_OPEN;
+ }
+ }
+ else { //FSM_BALISE_COURTE_OPEN
+ if (fsm_in.cw_psk31_done) {
+ next_state = FSM_OPEN2;
+ }
+ }
+
+ break;
+ default:
+ // Should never happen
+ next_state = FSM_OISIF;
+ break;
+ }
+
+
+ if (next_state != current_state) {
+ timestamp_state[next_state] = timestamp_now();
+
+ short_beacon_counter_last_update = 0;
+
+ fsm_state_switched(state_name(next_state));
+ }
+ current_state = next_state;
+}
+
+void fsm_update_inputs(struct fsm_input_signals_t* inputs)
+{
+ fsm_in = *inputs;
+}
+
+void fsm_get_outputs(struct fsm_output_signals_t* out)
+{
+ *out = fsm_out;
+}
+
+void fsm_balise_force() {
+ balise_state = BALISE_FSM_PENDING;
+}
+
+void fsm_balise_update() {
+
+ balise_fsm_state_t next_state = balise_state;
+
+ switch (balise_state) {
+ case BALISE_FSM_EVEN_HOUR:
+ if (fsm_in.hour_is_even == 0) {
+ next_state = BALISE_FSM_ODD_HOUR;
+ }
+ break;
+ case BALISE_FSM_ODD_HOUR:
+ if (fsm_in.hour_is_even == 1) {
+ if (timestamp_now() > 1000 * 60) { // Does not start the balise at startup
+ next_state = BALISE_FSM_PENDING;
+ }
+ else {
+ next_state = BALISE_FSM_EVEN_HOUR;
+ }
+ }
+ break;
+ case BALISE_FSM_PENDING:
+ if (current_state == FSM_BALISE_SPECIALE ||
+ current_state == FSM_BALISE_LONGUE) {
+ next_state = BALISE_FSM_EVEN_HOUR;
+ }
+ break;
+ default:
+ // Should never happen
+ next_state = BALISE_FSM_EVEN_HOUR;
+ break;
+ }
+
+ if (next_state != balise_state) {
+ fsm_state_switched(balise_state_name(next_state));
+ }
+
+ balise_state = next_state;
+}
+
+int fsm_sstv_update() {
+
+ sstv_fsm_state_t next_state = sstv_state;
+
+ switch (sstv_state) {
+ case SSTV_FSM_OFF:
+ if (fsm_in.sq && fsm_in.fax_mode) {
+ next_state = SSTV_FSM_ON;
+ }
+ break;
+ case SSTV_FSM_ON:
+ if (current_state == FSM_BALISE_LONGUE ||
+ current_state == FSM_ANTI_BAVARD ||
+ current_state == FSM_BALISE_SPECIALE ||
+ fsm_in.long_1750
+ ) {
+ next_state = SSTV_FSM_OFF;
+ }
+ break;
+
+ default:
+ // Should never happen
+ next_state = SSTV_FSM_OFF;
+ break;
+ }
+
+ if (next_state != sstv_state) {
+ fsm_state_switched(sstv_state_name(next_state));
+ }
+
+ sstv_state = next_state;
+
+ return sstv_state == SSTV_FSM_ON;
+}