aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/README.md6
-rw-r--r--src/common/Audio/cw.c155
-rw-r--r--src/common/Audio/cw.h36
-rw-r--r--src/common/Core/common.h3
-rw-r--r--src/common/Core/fsm.c180
-rw-r--r--src/common/Core/fsm.h13
-rw-r--r--src/common/Core/main.c87
-rw-r--r--src/common/Core/stats.c298
-rw-r--r--src/common/Core/stats.h41
-rw-r--r--src/common/sourcelist.txt1
10 files changed, 656 insertions, 164 deletions
diff --git a/src/README.md b/src/README.md
index 188bd89..f6733e7 100644
--- a/src/README.md
+++ b/src/README.md
@@ -41,6 +41,12 @@ Debug avec OpenOCD et GDB
load
continue
+Debug simulateur avec GDB
+=========================
+
+ handle SIGUSR1 nostop noignore noprint
+ handle SIG34 nostop noignore noprint
+
Analyse statique avec clang
===========================
diff --git a/src/common/Audio/cw.c b/src/common/Audio/cw.c
index 667d459..e7cc806 100644
--- a/src/common/Audio/cw.c
+++ b/src/common/Audio/cw.c
@@ -22,15 +22,15 @@
* SOFTWARE.
*/
-/* CW and PSK31 generator
+/* CW and BPSK{31,63,125} generator
*
* Concept:
*
* +-------------------+ +----------------+
- * | cw_push_message() | -> cw_msg_queue -> | cw_psk31task() | -> cw_audio_queue
+ * | cw_push_message() | -> cw_msg_queue -> | cw_psktask() | -> cw_audio_queue
* +-------------------+ +----------------+
*
- * The cw_psk31_fill_buffer() function can be called to fetch audio from the
+ * The cw_psk_fill_buffer() function can be called to fetch audio from the
* audio_queue
*/
@@ -119,12 +119,12 @@ const uint8_t cw_mapping[60] = { // {{{
0b1010111, // SK , ASCII '\'
}; //}}}
-#if ENABLE_PSK31
+#if ENABLE_PSK
/*
- * PSK31 Varicode
+ * PSK Varicode
* http://aintel.bi.ehu.es/psk31.html
*/
-const char *psk31_varicode[] = { // {{{
+const char *psk_varicode[] = { // {{{
"1010101011",
"1011011011",
"1011101101",
@@ -265,7 +265,7 @@ struct cw_message_s {
int freq;
- // If dit_duration is 0, the message is sent in PSK31
+ // If dit_duration is negative, the message is sent in PSK
int dit_duration;
};
@@ -274,15 +274,15 @@ QueueHandle_t cw_msg_queue;
// Queue that contains audio data
QueueHandle_t cw_audio_queue;
-static int cw_psk31_samplerate;
+static int cw_psk_samplerate;
static int cw_transmit_ongoing;
-static void cw_psk31_task(void *pvParameters);
+static void cw_psk_task(void *pvParameters);
-void cw_psk31_init(unsigned int samplerate)
+void cw_psk_init(unsigned int samplerate)
{
- cw_psk31_samplerate = samplerate;
+ cw_psk_samplerate = samplerate;
cw_transmit_ongoing = 0;
cw_msg_queue = xQueueCreate(10, sizeof(struct cw_message_s));
@@ -296,7 +296,7 @@ void cw_psk31_init(unsigned int samplerate)
}
xTaskCreate(
- cw_psk31_task,
+ cw_psk_task,
"CWPSKTask",
8*configMINIMAL_STACK_SIZE,
(void*) NULL,
@@ -349,12 +349,7 @@ size_t cw_symbol(uint8_t sym, uint8_t *on_buffer, size_t on_buffer_size)
return pos;
}
-// Transmit a string in morse code or PSK31.
-// Supported range for CW:
-// All ASCII between '+' and '\', which includes
-// numerals and capital letters.
-// Distinction between CW and PSK31 is done on dit_duration==0
-int cw_psk31_push_message(const char* text, int dit_duration, int frequency)
+int cw_psk_push_message(const char* text, int dit_duration, int frequency)
{
const int text_len = strlen(text);
@@ -375,7 +370,9 @@ int cw_psk31_push_message(const char* text, int dit_duration, int frequency)
msg.freq = frequency;
msg.dit_duration = dit_duration;
- xQueueSendToBack(cw_msg_queue, &msg, portMAX_DELAY); /* Send Message */
+ if (xQueueSendToBack(cw_msg_queue, &msg, portMAX_DELAY) != pdTRUE) {
+ trigger_fault(FAULT_SOURCE_CW_QUEUE);
+ }
cw_message_sent(msg.message);
@@ -407,17 +404,17 @@ static size_t cw_text_to_on_buffer(const char *msg, uint8_t *on_buffer, size_t o
}
-#if ENABLE_PSK31
+#if ENABLE_PSK
/*
* Turn a null terminated ASCII string into a uint8_t buffer
- * of 0 and 1 representing the PSK31 varicode for the input.
+ * of 0 and 1 representing the PSK varicode for the input.
*
* outstr must be at least size 20 + strlen(instr)*12 + 20 to accomodate
* the header and tail.
*
* Returns number of bytes written.
*/
-static size_t psk31_text_to_phase_buffer(const char* instr, uint8_t* outbits)
+static size_t psk_text_to_phase_buffer(const char* instr, uint8_t* outbits)
{
int i=0, j, k;
@@ -428,12 +425,14 @@ static size_t psk31_text_to_phase_buffer(const char* instr, uint8_t* outbits)
/* Encode the message, with 00 between letters */
for (j=0; j < strlen(instr); j++) {
- const char* varicode_bits = psk31_varicode[(int)instr[j]];
- for(k=0; k < strlen(varicode_bits); k++) {
- outbits[i++] = (varicode_bits[k] == '1') ? 1 : 0;
+ if (instr[j] < sizeof(psk_varicode)) {
+ const char* varicode_bits = psk_varicode[(int)instr[j]];
+ for(k=0; k < strlen(varicode_bits); k++) {
+ outbits[i++] = (varicode_bits[k] == '1') ? 1 : 0;
+ }
+ outbits[i++] = 0;
+ outbits[i++] = 0;
}
- outbits[i++] = 0;
- outbits[i++] = 0;
}
/* Tail of 0s */
@@ -446,7 +445,7 @@ static size_t psk31_text_to_phase_buffer(const char* instr, uint8_t* outbits)
#endif
-size_t cw_psk31_fill_buffer(int16_t *buf, size_t bufsize)
+size_t cw_psk_fill_buffer(int16_t *buf, size_t bufsize)
{
if (xQueueReceiveFromISR(cw_audio_queue, buf, NULL)) {
return bufsize;
@@ -457,7 +456,7 @@ size_t cw_psk31_fill_buffer(int16_t *buf, size_t bufsize)
}
static int16_t cw_audio_buf[AUDIO_BUF_LEN];
-static uint8_t cw_psk31_buffer[MAX_ON_BUFFER_LEN];
+static uint8_t cw_psk_buffer[MAX_ON_BUFFER_LEN];
static struct cw_message_s cw_fill_msg_current;
// Routine to generate CW audio
@@ -467,7 +466,7 @@ static int16_t cw_generate_audio(float omega, int i, int __attribute__ ((unused)
{
int16_t s = 0;
// Remove clicks from CW
- if (cw_psk31_buffer[i]) {
+ if (cw_psk_buffer[i]) {
const float remaining = 32768.0f - cw_generate_audio_ampl;
cw_generate_audio_ampl += remaining / 64.0f;
}
@@ -488,82 +487,93 @@ static int16_t cw_generate_audio(float omega, int i, int __attribute__ ((unused)
return s;
}
-#if ENABLE_PSK31
-static float psk31_generate_audio_nco = 0.0f;
-static int psk31_current_psk_phase = 1;
-static int16_t psk31_generate_audio(float omega, int i, int t, int samples_per_symbol)
+#if ENABLE_PSK
+static float psk_generate_audio_nco = 0.0f;
+static int psk_current_psk_phase = 1;
+static int16_t psk_generate_audio(float omega, int i, int t, int samples_per_symbol)
{
int16_t s = 0;
- const float base_ampl = 20000.0f;
- float psk31_generate_audio_ampl = 0.0f;
+ const float base_ampl = 10000.0f;
+ float psk_generate_audio_ampl = 0.0f;
- if (cw_psk31_buffer[i] == 1) {
- psk31_generate_audio_ampl = base_ampl;
+ if (cw_psk_buffer[i] == 1) {
+ psk_generate_audio_ampl = base_ampl;
}
else {
- psk31_generate_audio_ampl = base_ampl * arm_cos_f32(
+ psk_generate_audio_ampl = base_ampl * arm_cos_f32(
FLOAT_PI*(float)t/(float)samples_per_symbol);
}
- psk31_generate_audio_nco += omega;
- if (psk31_generate_audio_nco > FLOAT_PI) {
- psk31_generate_audio_nco -= 2.0f * FLOAT_PI;
+ psk_generate_audio_nco += omega;
+ if (psk_generate_audio_nco > FLOAT_PI) {
+ psk_generate_audio_nco -= 2.0f * FLOAT_PI;
}
- s = psk31_generate_audio_ampl *
- arm_sin_f32(psk31_generate_audio_nco +
- (psk31_current_psk_phase == 1 ? 0.0f : FLOAT_PI));
+ s = psk_generate_audio_ampl *
+ arm_sin_f32(psk_generate_audio_nco +
+ (psk_current_psk_phase == 1 ? 0.0f : FLOAT_PI));
return s;
}
#endif
-static void cw_psk31_task(void __attribute__ ((unused))*pvParameters)
+static void cw_psk_task(void __attribute__ ((unused))*pvParameters)
{
- int buf_pos = 0;
+ int buf_pos = 0;
while (1) {
int status = xQueueReceive(cw_msg_queue, &cw_fill_msg_current, portMAX_DELAY);
if (status == pdTRUE) {
-
- size_t cw_psk31_buffer_len = 0;
+ size_t cw_psk_buffer_len = 0;
cw_transmit_ongoing = 1;
- if (cw_fill_msg_current.dit_duration) {
- cw_psk31_buffer_len = cw_text_to_on_buffer(
+ if (cw_fill_msg_current.dit_duration == 0) {
+ // Illegal
+ cw_transmit_ongoing = 0;
+ continue;
+ }
+ else if (cw_fill_msg_current.dit_duration > 0) {
+ cw_psk_buffer_len = cw_text_to_on_buffer(
cw_fill_msg_current.message,
- cw_psk31_buffer,
+ cw_psk_buffer,
MAX_ON_BUFFER_LEN);
}
-#if ENABLE_PSK31
+#if ENABLE_PSK
else {
- cw_psk31_buffer_len = psk31_text_to_phase_buffer(
+ cw_psk_buffer_len = psk_text_to_phase_buffer(
cw_fill_msg_current.message,
- cw_psk31_buffer);
+ cw_psk_buffer);
}
#endif
// Angular frequency of NCO
const float omega = 2.0f * FLOAT_PI * cw_fill_msg_current.freq /
- (float)cw_psk31_samplerate;
+ (float)cw_psk_samplerate;
- const int samples_per_symbol = (cw_fill_msg_current.dit_duration != 0) ?
- (cw_psk31_samplerate * cw_fill_msg_current.dit_duration) / 1000 :
+ const int samples_per_symbol = (cw_fill_msg_current.dit_duration == -1) ?
/* BPSK31 is at 31.25 symbols per second. */
- cw_psk31_samplerate * 100 / 3125;
-
-#if ENABLE_PSK31
- psk31_current_psk_phase = 1;
+ cw_psk_samplerate * 100 / 3125 :
+ (cw_fill_msg_current.dit_duration == -2) ?
+ /* BPSK63 is at 2*31.25 symbols per second. */
+ cw_psk_samplerate * 50 / 3125 :
+ (cw_fill_msg_current.dit_duration == -3) ?
+ /* BPSK125 is at 4*31.25 symbols per second. */
+ cw_psk_samplerate * 25 / 3125 :
+ /* CW directly depends on dit_duration, which is in ms */
+ (cw_psk_samplerate * cw_fill_msg_current.dit_duration) / 1000;
+
+#if ENABLE_PSK
+ psk_current_psk_phase = 1;
#endif
- for (int i = 0; i < cw_psk31_buffer_len; i++) {
+ for (int i = 0; i < cw_psk_buffer_len; i++) {
for (int t = 0; t < samples_per_symbol; t++) {
-#if ENABLE_PSK31
- int16_t s = (cw_fill_msg_current.dit_duration != 0) ?
+#if ENABLE_PSK
+ int16_t s = (cw_fill_msg_current.dit_duration > 0) ?
cw_generate_audio(omega, i, t) :
- psk31_generate_audio(omega, i, t, samples_per_symbol);
+ psk_generate_audio(omega, i, t, samples_per_symbol);
#else
int16_t s = cw_generate_audio(omega, i, t);
#endif
@@ -571,9 +581,9 @@ static void cw_psk31_task(void __attribute__ ((unused))*pvParameters)
// Stereo
for (int channel = 0; channel < 2; channel++) {
if (buf_pos == AUDIO_BUF_LEN) {
- // It should take AUDIO_BUF_LEN/cw_psk31_samplerate seconds to send one buffer.
+ // It should take AUDIO_BUF_LEN/cw_psk_samplerate seconds to send one buffer.
// If it takes more than 4 times as long, we think there is a problem.
- const TickType_t reasonable_delay = pdMS_TO_TICKS(4000 * AUDIO_BUF_LEN / cw_psk31_samplerate);
+ const TickType_t reasonable_delay = pdMS_TO_TICKS(4000 * AUDIO_BUF_LEN / cw_psk_samplerate);
if (xQueueSendToBack(cw_audio_queue, &cw_audio_buf, reasonable_delay) != pdTRUE) {
trigger_fault(FAULT_SOURCE_CW_AUDIO_QUEUE);
}
@@ -584,21 +594,20 @@ static void cw_psk31_task(void __attribute__ ((unused))*pvParameters)
}
-#if ENABLE_PSK31
- if (cw_psk31_buffer[i] == 0) {
- psk31_current_psk_phase *= -1;
+#if ENABLE_PSK
+ if (cw_psk_buffer[i] == 0) {
+ psk_current_psk_phase *= -1;
}
#endif
}
// We have completed this message
-
cw_transmit_ongoing = 0;
}
}
}
-int cw_psk31_busy(void)
+int cw_psk_busy(void)
{
return cw_transmit_ongoing;
}
diff --git a/src/common/Audio/cw.h b/src/common/Audio/cw.h
index 39be9c5..153f219 100644
--- a/src/common/Audio/cw.h
+++ b/src/common/Audio/cw.h
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
- * Copyright (c) 2015 Matthias P. Braendli
+ * Copyright (c) 2019 Matthias P. Braendli
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,32 +22,38 @@
* SOFTWARE.
*/
-#ifndef __CW_H_
-#define __CW_H_
+#pragma once
#include <stdint.h>
#include <stddef.h>
// Setup the CW generator to create audio samples at the given
// samplerate.
-void cw_psk31_init(unsigned int samplerate);
-
-// Append new CW or PSK31 text to transmit
-// CW/PSK31 audio centre frequency in Hz
-// if dit_duration == 0, message is sent in PSK31
+void cw_psk_init(unsigned int samplerate);
+
+// Append new CW or PSK text to transmit
+// CW/PSK audio centre frequency in Hz
+//
+// Supported characters for CW:
+// All ASCII between '+' and '\', which includes
+// numerals and capital letters.
+//
+// Supported characters for PSK: 7-bit clean ASCII
+//
+// if dit_duration == -1, message is sent in PSK31
+// if dit_duration == -2, message is sent in PSK63
+// if dit_duration == -3, message is sent in PSK125
// otherwise it is sent in CW, with dit_duration in ms
// returns 0 on failure, 1 on success
-int cw_psk31_push_message(const char* text, int frequency, int dit_duration);
+int cw_psk_push_message(const char* text, int frequency, int dit_duration);
-// Write the waveform into the buffer (stereo), both for cw and psk31
-size_t cw_psk31_fill_buffer(int16_t *buf, size_t bufsize);
+// Write the waveform into the buffer (stereo), both for cw and psk
+size_t cw_psk_fill_buffer(int16_t *buf, size_t bufsize);
-// Return 1 if the CW or PSK31 generator is running
-int cw_psk31_busy(void);
+// Return 1 if the CW or PSK generator is running
+int cw_psk_busy(void);
void cw_message_sent(const char*);
size_t cw_symbol(uint8_t, uint8_t *, size_t);
-#endif // __CW_H_
-
diff --git a/src/common/Core/common.h b/src/common/Core/common.h
index 4918a5b..662ecad 100644
--- a/src/common/Core/common.h
+++ b/src/common/Core/common.h
@@ -32,7 +32,7 @@
#include <time.h>
/* Feature defines */
-#define ENABLE_PSK31 0
+#define ENABLE_PSK 1
#define FLOAT_PI 3.1415926535897932384f
@@ -66,6 +66,7 @@ int random_bool(void);
#define FAULT_SOURCE_TIM6_ISR 8
#define FAULT_SOURCE_ADC2_QUEUE 9
#define FAULT_SOURCE_ADC2_IRQ 10
+#define FAULT_SOURCE_CW_QUEUE 11
void trigger_fault(int source);
int find_last_sunday(const struct tm*);
diff --git a/src/common/Core/fsm.c b/src/common/Core/fsm.c
index 9fe2c4c..a41ee0e 100644
--- a/src/common/Core/fsm.c
+++ b/src/common/Core/fsm.c
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
- * Copyright (c) 2018 Matthias P. Braendli, Maximilien Cuony
+ * Copyright (c) 2019 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
@@ -27,6 +27,7 @@
#include <stdint.h>
#include "Core/common.h"
#include "Core/fsm.h"
+#include "Core/stats.h"
#include "GPIO/usart.h"
#include "GPIO/temperature.h"
#include "GPIO/analog.h"
@@ -44,8 +45,18 @@ 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];
+#define BALISE_MESSAGE_LEN 64
+static char balise_message[BALISE_MESSAGE_LEN];
+
+static int balise_message_empty(void)
+{
+ return balise_message[0] == '\0';
+}
+
+static void balise_message_clear(void)
+{
+ balise_message[0] = '\0';
+}
// Each 20 minutes, send a SHORT_BEACON
@@ -84,6 +95,7 @@ void fsm_init() {
current_state = FSM_OISIF;
balise_state = BALISE_FSM_EVEN_HOUR;
sstv_state = SSTV_FSM_OFF;
+ balise_message[0] = '\0';
qso_info.qso_occurred = 0;
qso_info.qso_start_time = timestamp_now();
@@ -113,6 +125,9 @@ static const char* state_name(fsm_state_t state) {
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_STATS1: return "FSM_BALISE_STATS1";
+ case FSM_BALISE_STATS2: return "FSM_BALISE_STATS2";
+ case FSM_BALISE_STATS3: return "FSM_BALISE_STATS3";
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";
@@ -141,6 +156,9 @@ static fsm_state_t select_grande_balise(void) {
if (fsm_in.qrp || fsm_in.swr_high) {
return FSM_BALISE_SPECIALE;
}
+ else if (fsm_in.send_stats) {
+ return FSM_BALISE_STATS1;
+ }
else {
return FSM_BALISE_LONGUE;
}
@@ -188,7 +206,7 @@ void fsm_update() {
// Some defaults for the outgoing signals
fsm_out.tx_on = 0;
fsm_out.modulation = 0;
- fsm_out.cw_psk31_trigger = 0;
+ fsm_out.cw_psk_trigger = 0;
fsm_out.cw_dit_duration = 50;
fsm_out.msg_frequency = 960;
fsm_out.require_tone_detector = 0;
@@ -262,9 +280,9 @@ void fsm_update() {
// The letter 'G' is a bit different
fsm_out.msg_frequency = 696;
}
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
next_state = FSM_ECOUTE;
}
break;
@@ -364,9 +382,10 @@ void fsm_update() {
// Short post-delay to underscore the fact that
// transmission was forcefully cut off.
fsm_out.msg = " HI HI ";
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
+ stats_anti_bavard_triggered();
next_state = FSM_BLOQUE;
}
break;
@@ -384,13 +403,13 @@ void fsm_update() {
fsm_out.msg_frequency = 696;
fsm_out.cw_dit_duration = 70;
fsm_out.msg = " 73" CW_POSTDELAY;
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
if (fsm_in.sq) {
next_state = FSM_QSO;
qso_info.qso_start_time = timestamp_now();
}
- else if (fsm_in.cw_psk31_done) {
+ else if (fsm_in.cw_psk_done) {
next_state = FSM_OISIF;
}
break;
@@ -403,13 +422,13 @@ void fsm_update() {
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;
+ fsm_out.cw_psk_trigger = 1;
if (fsm_in.sq) {
next_state = FSM_QSO;
qso_info.qso_start_time = timestamp_now();
}
- else if (fsm_in.cw_psk31_done) {
+ else if (fsm_in.cw_psk_done) {
next_state = FSM_OISIF;
}
break;
@@ -429,28 +448,33 @@ void fsm_update() {
else {
fsm_out.msg = " HB9G JN36BK" CW_POSTDELAY;
}
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
if (fsm_in.sq) {
next_state = FSM_QSO;
qso_info.qso_start_time = timestamp_now();
}
- else if (fsm_in.cw_psk31_done) {
+ else if (fsm_in.cw_psk_done) {
next_state = FSM_OISIF;
}
break;
case FSM_BALISE_LONGUE:
+ case FSM_BALISE_STATS1:
fsm_out.tx_on = 1;
fsm_out.msg_frequency = 588;
- fsm_out.cw_dit_duration = 110;
+#warning "dit duration = 110"
+ fsm_out.cw_dit_duration = 30;
{
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) {
+ const char *eol_info = "73";
+ if (current_state == FSM_BALISE_STATS1) {
+ eol_info = "PSK125";
+ }
+ else if (!fsm_in.wind_generator_ok) {
eol_info = "\\";
// The backslash is the SK digraph
}
@@ -466,32 +490,93 @@ void fsm_update() {
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);
+ if (balise_message_empty()) {
+#warning "only for debug"
+ if (current_state == FSM_BALISE_STATS1) {
+ snprintf(balise_message, BALISE_MESSAGE_LEN-1,
+ CW_PREDELAY "HB9G " CW_POSTDELAY);
+ }
+ else
+ if (temperature_valid()) {
+ snprintf(balise_message, BALISE_MESSAGE_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(balise_message, BALISE_MESSAGE_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 = balise_message;
+
+ last_supply_voltage_decivolts = supply_decivolts;
+
+ fsm_out.cw_psk_trigger = 1;
+ }
+
+ if (fsm_in.cw_psk_done) {
+ balise_message_clear();
+ // The exercise_fsm loop needs to see a 1 to 0 transition on cw_psk_trigger
+ // so that it considers the STATS2 message.
+ fsm_out.cw_psk_trigger = 0;
+ if (current_state == FSM_BALISE_STATS1) {
+ fsm_out.msg = NULL;
+ next_state = FSM_BALISE_STATS2;
}
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);
+ next_state = FSM_OISIF;
}
+ }
+ break;
+
+ case FSM_BALISE_STATS2:
+ fsm_out.tx_on = 1;
+ fsm_out.msg_frequency = 588;
+ fsm_out.cw_dit_duration = -3; // PSK125
- fsm_out.msg = cw_message_balise;
+ // All predecessor states must NULL the fsm_out.msg field!
+ if (fsm_out.msg == NULL) {
+ fsm_out.msg = stats_build_text();
+ }
+ fsm_out.cw_psk_trigger = 1;
- last_supply_voltage_decivolts = supply_decivolts;
+ if (fsm_in.cw_psk_done) {
+ fsm_out.cw_psk_trigger = 0;
+ next_state = FSM_BALISE_STATS3;
+ }
+ break;
- fsm_out.cw_psk31_trigger = 1;
+ case FSM_BALISE_STATS3:
+ fsm_out.tx_on = 1;
+ fsm_out.msg_frequency = 588;
+ fsm_out.cw_dit_duration = 110;
+
+ if (balise_message_empty()) {
+ const char *eol_info = "73";
+ if (!fsm_in.wind_generator_ok) {
+ eol_info = "\\";
+ // The backslash is the SK digraph
+ }
+ snprintf(balise_message, BALISE_MESSAGE_LEN-1,
+ CW_PREDELAY "%s" CW_POSTDELAY,
+ eol_info);
+ fsm_out.msg = balise_message;
+ fsm_out.cw_psk_trigger = 1;
}
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
+ stats_beacon_sent();
+ fsm_out.cw_psk_trigger = 1;
+ balise_message_clear();
next_state = FSM_OISIF;
}
break;
@@ -501,28 +586,30 @@ void fsm_update() {
fsm_out.msg_frequency = 696;
fsm_out.cw_dit_duration = 70;
- {
+ if (balise_message_empty()) {
const float supply_voltage = round_float_to_half_steps(analog_measure_12v());
const int supply_decivolts = supply_voltage * 10.0f;
- char *eol_info = "73";
+ const 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,
+ snprintf(balise_message, BALISE_MESSAGE_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.msg = balise_message;
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
}
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
+ stats_beacon_sent();
+ balise_message_clear();
next_state = FSM_OISIF;
}
break;
@@ -551,14 +638,15 @@ void fsm_update() {
fsm_out.msg = CW_PREDELAY "HB9G JN36BK 1628M" CW_POSTDELAY;
}
}
- fsm_out.cw_psk31_trigger = 1;
+ fsm_out.cw_psk_trigger = 1;
if (current_state == FSM_BALISE_COURTE) {
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
if (fsm_in.sq) {
next_state = FSM_OPEN2;
}
else {
+ stats_beacon_sent();
next_state = FSM_OISIF;
}
}
@@ -567,7 +655,8 @@ void fsm_update() {
}
}
else { //FSM_BALISE_COURTE_OPEN
- if (fsm_in.cw_psk31_done) {
+ if (fsm_in.cw_psk_done) {
+ stats_beacon_sent();
next_state = FSM_OPEN2;
}
}
@@ -626,7 +715,8 @@ void fsm_balise_update() {
break;
case BALISE_FSM_PENDING:
if (current_state == FSM_BALISE_SPECIALE ||
- current_state == FSM_BALISE_LONGUE) {
+ current_state == FSM_BALISE_LONGUE ||
+ current_state == FSM_BALISE_STATS3) {
next_state = BALISE_FSM_EVEN_HOUR;
}
break;
diff --git a/src/common/Core/fsm.h b/src/common/Core/fsm.h
index 37ca386..ba723b1 100644
--- a/src/common/Core/fsm.h
+++ b/src/common/Core/fsm.h
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
- * Copyright (c) 2016 Matthias P. Braendli, Maximilien Cuony
+ * Copyright (c) 2019 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
@@ -40,6 +40,9 @@ enum fsm_state_e {
FSM_TEXTE_HB9G, // Transmit HB9G after QSO
FSM_TEXTE_LONG, // Transmit either HB9G JN36BK or HB9G 1628M after QSO
FSM_BALISE_LONGUE, // Full-length 2-hour beacon
+ FSM_BALISE_STATS1, // Full-length 2-hour beacon at 22:00, 1st part in CW
+ FSM_BALISE_STATS2, // Full-length 2-hour beacon at 22:00, 2nd part in PSK
+ FSM_BALISE_STATS3, // Full-length 2-hour beacon at 22:00, 3nd part in CW
FSM_BALISE_SPECIALE, // 2-hour beacon when in QRP or with high power return mode
FSM_BALISE_COURTE, // Short intermittent beacon
FSM_BALISE_COURTE_OPEN, // Short intermittent beacon, need to switch to OPEN
@@ -74,6 +77,7 @@ struct fsm_input_signals_t {
int discrim_u; // FM discriminator says RX is too high in frequency
int qrp; // The relay is currently running with low power
int hour_is_even; // 1 if hour is even
+ int send_stats; // 1 if the balise should contain stats
float temp; // temperature in degrees C
float humidity; // relative humidity, range [0-100] %
int wind_generator_ok; // false if the generator is folded out of the wind
@@ -86,7 +90,7 @@ struct fsm_input_signals_t {
int long_1750; // 1750Hz detected for more than 5s
/* Signals coming from CW and PSK generator */
- int cw_psk31_done; // The CW and PSK generator has finished transmitting the message
+ int cw_psk_done; // The CW and PSK generator has finished transmitting the message
/* Signal coming from the standing wave ratio meter */
int swr_high; // We see a lot of return power
@@ -102,9 +106,8 @@ struct fsm_output_signals_t {
/* Signals to the CW and PSK generator */
const char* msg; // The message to transmit
int msg_frequency; // What audio frequency for the CW or PSK message
- int cw_dit_duration; // CW speed, dit duration in ms
- int cw_psk31_trigger; // Set to true to trigger a CW or PSK31 transmission.
- // PSK31 is sent if cw_dit_duration is 0
+ int cw_dit_duration; // CW speed, dit duration in ms or PSK speed (see enum cw_psk_types_e)
+ int cw_psk_trigger; // Set to true to trigger a CW or PSK transmission.
/* Tone detector */
int require_tone_detector; // Enables audio input and detector for DTMF and 1750
diff --git a/src/common/Core/main.c b/src/common/Core/main.c
index 07657fa..f4125d1 100644
--- a/src/common/Core/main.c
+++ b/src/common/Core/main.c
@@ -42,6 +42,7 @@
#include "GPIO/i2c.h"
#include "GPS/gps.h"
#include "Core/fsm.h"
+#include "Core/stats.h"
#include "Core/common.h"
#include "GPIO/usart.h"
#include "Core/delay.h"
@@ -140,7 +141,7 @@ int main(void) {
static void launcher_task(void __attribute__ ((unused))*pvParameters)
{
usart_debug_puts("CW init\r\n");
- cw_psk31_init(16000);
+ cw_psk_init(16000);
usart_debug_puts("PIO init\r\n");
pio_init();
@@ -356,7 +357,7 @@ static void audio_callback(void __attribute__ ((unused))*context, int select_buf
leds_turn_on(LED_RED);
}
- size_t samples_len = cw_psk31_fill_buffer(samples, AUDIO_BUF_LEN);
+ size_t samples_len = cw_psk_fill_buffer(samples, AUDIO_BUF_LEN);
if (samples_len == 0) {
for (int i = 0; i < AUDIO_BUF_LEN; i++) {
@@ -380,7 +381,6 @@ static void audio_callback(void __attribute__ ((unused))*context, int select_buf
}
timestamp_last_audio_callback = timestamp_now();
-
}
static struct tm gps_time;
@@ -408,15 +408,6 @@ static void gps_monit_task(void __attribute__ ((unused))*pvParameters) {
while (1) {
const uint64_t now = timestamp_now();
- if (last_volt_and_temp_timestamp + 20000 < now) {
- usart_debug("ALIM %d mV\r\n", (int)roundf(1000.0f * analog_measure_12v()));
-
- const float temp = temperature_get();
- usart_debug("TEMP %d.%02d\r\n", (int)temp, (int)(temp * 100.0f - (int)(temp) * 100.0f));
-
- last_volt_and_temp_timestamp = now;
- }
-
struct tm time = {0};
int time_valid = local_time(&time);
int derived_mode = 0;
@@ -463,6 +454,22 @@ static void gps_monit_task(void __attribute__ ((unused))*pvParameters) {
last_hour_is_even_change_timestamp = now;
}
+ if (last_volt_and_temp_timestamp + 20000 < now) {
+ const float u_bat = analog_measure_12v();
+ usart_debug("ALIM %d mV\r\n", (int)roundf(1000.0f * u_bat));
+
+ stats_voltage(u_bat);
+ if (time_valid && time.tm_min == 0) {
+ stats_voltage_at_full_hour(time.tm_hour, u_bat);
+ }
+
+ const float temp = temperature_get();
+ stats_temp(temp);
+ usart_debug("TEMP %d.%02d\r\n", (int)temp, (int)(temp * 100.0f - (int)(temp) * 100.0f));
+
+ last_volt_and_temp_timestamp = now;
+ }
+
int num_sv_used = 0;
gps_utctime(&gps_time, &num_sv_used);
@@ -531,6 +538,7 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
int cw_last_trigger = 0;
int last_tm_trigger_button = 0;
+ int last_tx_on = 0;
int last_sq = 0;
int last_qrp = 0;
int last_cw_done = 0;
@@ -538,6 +546,8 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
int last_discrim_u = 0;
int last_wind_generator_ok = 0;
+ uint64_t last_qrp_stats_updated = timestamp_now();
+
fsm_input.humidity = 0;
fsm_input.temp = 15;
fsm_input.swr_high = 0;
@@ -549,6 +559,14 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
pio_set_fsm_signals(&fsm_input);
+ const uint64_t now = timestamp_now();
+
+ // QRP/QRO doesn't change too often, updating every 10s is good enough
+ if (last_qrp_stats_updated + 10000 < now) {
+ stats_qrp(fsm_input.qrp);
+ last_qrp_stats_updated = now;
+ }
+
if (last_sq != fsm_input.sq) {
last_sq = fsm_input.sq;
usart_debug("In SQ %d\r\n", last_sq);
@@ -567,30 +585,26 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
}
if (last_wind_generator_ok != fsm_input.wind_generator_ok) {
last_wind_generator_ok = fsm_input.wind_generator_ok;
+ stats_wind_generator_moved();
usart_debug("In eolienne %s\r\n", last_wind_generator_ok ? "vent" : "replie");
}
- if (tm_trigger_button == 1 && last_tm_trigger_button == 0) {
- fsm_balise_force();
- }
- last_tm_trigger_button = tm_trigger_button;
-
- const int cw_psk31_done = !cw_psk31_busy();
- const int cw_done = cw_psk31_done && only_zero_in_audio_buffer;
+ const int cw_psk_done = !cw_psk_busy();
+ const int cw_done = cw_psk_done && only_zero_in_audio_buffer;
// Set the done flag to 1 only once, when cw_done switches from 0 to 1
if (last_cw_done != cw_done) {
usart_debug("In cw_done change %d %d\r\n", cw_done, only_zero_in_audio_buffer);
if (cw_done) {
- fsm_input.cw_psk31_done = cw_done;
+ fsm_input.cw_psk_done = cw_done;
leds_turn_off(LED_ORANGE);
}
last_cw_done = cw_done;
}
else {
- fsm_input.cw_psk31_done = 0;
+ fsm_input.cw_psk_done = 0;
}
@@ -610,7 +624,23 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
fsm_input.swr_high = swr_error_flag;
fsm_input.hour_is_even = hour_is_even;
+ struct tm time = {0};
+ int time_valid = local_time(&time);
+ if (!time_valid) {
+ time_valid = local_derived_time(&time);
+ }
+ if (time_valid) {
+ fsm_input.send_stats = (time.tm_hour == 22) ? 1 : 0;
+ }
+
fsm_update_inputs(&fsm_input);
+
+ if (tm_trigger_button == 1 && last_tm_trigger_button == 0) {
+ fsm_update_inputs(&fsm_input);
+ fsm_balise_force();
+ }
+ last_tm_trigger_button = tm_trigger_button;
+
fsm_update();
fsm_balise_update();
const int disable_1750_filter = fsm_sstv_update();
@@ -620,16 +650,23 @@ static void exercise_fsm(void __attribute__ ((unused))*pvParameters)
fsm_get_outputs(&fsm_out);
pio_set_tx(fsm_out.tx_on);
+ if (fsm_out.tx_on != last_tx_on) {
+ stats_tx_switched(fsm_out.tx_on);
+ last_tx_on = fsm_out.tx_on;
+ }
pio_set_mod_off(!fsm_out.modulation);
// Add message to CW generator only on rising edge of trigger
- if (fsm_out.cw_psk31_trigger && !cw_last_trigger) {
- cw_psk31_push_message(fsm_out.msg, fsm_out.cw_dit_duration, fsm_out.msg_frequency);
+ if (fsm_out.cw_psk_trigger && !cw_last_trigger && fsm_out.msg != NULL) {
+ fprintf(stderr, "TRIG CW %s\n", fsm_out.msg);
+ const int success = cw_psk_push_message(fsm_out.msg, fsm_out.cw_dit_duration, fsm_out.msg_frequency);
+ if (!success) {
+ usart_debug_puts("cw_psk_push_message failed");
+ }
leds_turn_on(LED_ORANGE);
}
- cw_last_trigger = fsm_out.cw_psk31_trigger;
-
+ cw_last_trigger = fsm_out.cw_psk_trigger;
}
}
diff --git a/src/common/Core/stats.c b/src/common/Core/stats.c
new file mode 100644
index 0000000..9a14076
--- /dev/null
+++ b/src/common/Core/stats.c
@@ -0,0 +1,298 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2019 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/stats.h"
+#include "Core/common.h"
+#include "vc.h"
+
+static int values_valid = 0;
+static int num_beacons_sent = 0;
+static int num_wind_generator_movements = 0;
+static int num_tx_switch = 0;
+static int num_antibavard = 0;
+static int num_sv_used = 0;
+static int num_qro = 0;
+static int num_qrp = 0;
+static float battery_min = -1.0f;
+static float battery_max = -1.0f;
+static float battery_per_hour[24];
+static float temp_min = -1.0f;
+static float temp_max = -1.0f;
+
+static int last_tx_on_valid = 0;
+static uint64_t last_tx_on = 0;
+static uint64_t max_qso_duration = 0;
+
+/* Ideas
+ *
+ * Version
+ * Uptime
+ * Number of beacons
+ * Ubat min/max/avg
+ * Temperature min/max/avg
+ * QRP/QRO time ratio in %
+ * Number of K, G, D, U, S, R sent
+ * Number of TX On/Off
+ * How many times anti-bavard got triggered
+ * Max SWR ratio
+ * Number of wind generator movements
+ * Longest QSO duration
+ * Number of GNSS SVs tracked
+ */
+
+#define STATS_LEN 1024 // also check MAX_MESSAGE_LEN in cw.c
+static char stats_text[STATS_LEN];
+static int32_t stats_end_ix = 0;
+
+static void clear_stats()
+{
+ num_beacons_sent = 0;
+ num_wind_generator_movements = 0;
+ num_tx_switch = 0;
+ num_antibavard = 0;
+ battery_min = -1.0f;
+ battery_max = -1.0f;
+ temp_min = -1.0f;
+ temp_max = -1.0f;
+ num_qro = 0;
+ num_qrp = 0;
+ last_tx_on_valid = 0;
+ max_qso_duration = 0;
+ for (int i = 0; i < 24; i++) {
+ battery_per_hour[i] = -1.0f;
+ }
+ values_valid = 1;
+}
+
+void stats_voltage_at_full_hour(int hour, float u_bat)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ if (hour > 0 && hour < 24) {
+ battery_per_hour[hour] = u_bat;
+ }
+}
+
+void stats_voltage(float u_bat)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ if (u_bat < battery_min || battery_min == -1.0f) {
+ battery_min = u_bat;
+ }
+
+ if (u_bat > battery_max || battery_max == -1.0f) {
+ battery_max = u_bat;
+ }
+}
+
+void stats_temp(float temp)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ if (temp < temp_min || temp_min == -1.0f) {
+ temp_min = temp;
+ }
+
+ if (temp > temp_max || temp_max == -1.0f) {
+ temp_max = temp;
+ }
+}
+
+void stats_qrp(int is_qrp)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ if (is_qrp) {
+ num_qrp++;
+ }
+ else {
+ num_qro++;
+ }
+}
+
+void stats_wind_generator_moved()
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ num_wind_generator_movements++;
+}
+
+void stats_beacon_sent()
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ num_beacons_sent++;
+}
+
+void stats_tx_switched(int tx_on)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ num_tx_switch++;
+
+ if (tx_on) {
+ last_tx_on = timestamp_now();
+ fprintf(stderr, "TX on at %lu\n", last_tx_on);
+ last_tx_on_valid = 1;
+ }
+ else if (last_tx_on_valid) {
+ const uint64_t qso_duration = timestamp_now() - last_tx_on;
+ fprintf(stderr, "TX off with dur=%lu\n", qso_duration);
+ if (qso_duration > max_qso_duration) {
+ max_qso_duration = qso_duration;
+ }
+ }
+}
+
+void stats_anti_bavard_triggered()
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ num_antibavard++;
+}
+
+void stats_num_gnss_sv(int num_sv)
+{
+ if (values_valid == 0) {
+ clear_stats();
+ }
+
+ num_sv_used = num_sv;
+}
+
+const char* stats_build_text(void)
+{
+ struct tm time = {0};
+ int time_valid = local_time(&time);
+
+ uint64_t uptime = timestamp_now();
+ int uptime_j = uptime / (24 * 3600 * 1000);
+ uptime -= uptime_j * (24 * 3600 * 1000);
+ int uptime_h = uptime / (3600 * 1000);
+ uptime -= uptime_h * (24 * 3600 * 1000);
+ int uptime_m = uptime / (60 * 1000);
+
+ stats_end_ix = snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "HB9G www.glutte.ch HB9G www.glutte.ch\n");
+
+ if (time_valid) {
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "Statistiques du %04d-%02d-%02d\n",
+ time.tm_year + 1900, time.tm_mon + 1, time.tm_mday);
+ }
+ else {
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "Statistiques de la journee\n");
+ }
+
+ const int battery_min_decivolt = 10.0f * battery_min;
+ const int battery_max_decivolt = 10.0f * battery_max;
+
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "Version= %s\n"
+ "Uptime= %dj%dh%dm\n",
+ vc_get_version(),
+ uptime_j, uptime_h, uptime_m);
+
+ if (values_valid == 0) {
+ return stats_text;
+ }
+
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "U min,max= %dV%01d,%dV%01d\n"
+ "Temps QRP= %d%%\n",
+ battery_min_decivolt / 10, battery_min_decivolt % 10,
+ battery_max_decivolt / 10, battery_max_decivolt % 10,
+ 100 * num_qrp / (num_qrp + num_qro));
+
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "U heures pleines= ");
+
+ for (int h = 0; h < 24; h++) {
+ if (battery_per_hour[h] == -1.0f) {
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ " ?");
+ }
+ else {
+ const int battery_decivolts = 10.0f * battery_per_hour[h];
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ " %dV%01d",
+ battery_decivolts / 10, battery_decivolts % 10);
+ }
+ }
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "\n");
+
+ const int temp_min_decidegree = 10.0f * temp_min;
+ const int temp_max_decidegree = 10.0f * temp_max;
+
+ uint64_t qso_duration = max_qso_duration;
+ int qso_duration_h = qso_duration / (3600 * 1000);
+ qso_duration -= qso_duration_h * ( 3600 * 1000);
+ int qso_duration_m = qso_duration / (60 * 1000);
+ qso_duration -= qso_duration_m * (60 * 1000);
+ int qso_duration_s = qso_duration / (1000);
+
+ stats_end_ix += snprintf(stats_text + stats_end_ix, STATS_LEN - 1 - stats_end_ix,
+ "Nbre de commutations eolienne= %d\n"
+ "Temp min,max= %dC%01d,%dC%01d\n"
+ "Nbre de balises= %d\n"
+ "Nbre de TX ON/OFF= %d\n"
+ "Nbre anti-bavard= %d\n"
+ "QSO le plus long= %dh%dm%ds\n"
+ "Sat GPS= %d\n",
+ num_wind_generator_movements,
+ temp_min_decidegree / 10, temp_min_decidegree % 10,
+ temp_max_decidegree / 10, temp_max_decidegree % 10,
+ num_beacons_sent,
+ num_tx_switch,
+ num_antibavard,
+ qso_duration_h, qso_duration_m, qso_duration_s,
+ num_sv_used
+ );
+
+ values_valid = 0;
+
+ return stats_text;
+}
diff --git a/src/common/Core/stats.h b/src/common/Core/stats.h
new file mode 100644
index 0000000..09f4c17
--- /dev/null
+++ b/src/common/Core/stats.h
@@ -0,0 +1,41 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2019 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.
+ */
+
+#pragma once
+#include <stdint.h>
+
+void stats_voltage_at_full_hour(int hour, float u_bat);
+void stats_voltage(float u_bat);
+
+void stats_temp(float temp);
+void stats_wind_generator_moved(void);
+void stats_beacon_sent(void);
+void stats_tx_switched(int tx_on);
+void stats_anti_bavard_triggered(void);
+void stats_num_gnss_sv(int num_sv);
+
+// Must be called in regular intervals
+void stats_qrp(int is_qrp);
+
+const char* stats_build_text(void);
diff --git a/src/common/sourcelist.txt b/src/common/sourcelist.txt
index b0010be..47526cd 100644
--- a/src/common/sourcelist.txt
+++ b/src/common/sourcelist.txt
@@ -5,6 +5,7 @@ GPS/gps.c
GPS/minmea.c
Core/common.c
Core/fsm.c
+Core/stats.c
Core/main.c
Audio/cw.c
Audio/audio.c