SubCar fm mode (#2965)

This commit is contained in:
Totoo
2026-02-06 17:20:14 +01:00
committed by GitHub
parent 6562e596e8
commit fe9f0c228f
8 changed files with 353 additions and 153 deletions
+18 -2
View File
@@ -91,6 +91,7 @@ SubCarView::SubCarView(NavigationView& nav)
&button_clear_list,
&check_log,
&labels,
&options_mode,
&recent_entries_view});
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
@@ -114,11 +115,17 @@ SubCarView::SubCarView(NavigationView& nav)
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
nav_.push<SubCarRecentEntryDetailView>(entry);
};
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
receiver_model.enable();
options_mode.on_change = [this](size_t, int32_t v) {
modulation = v;
chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes
baseband::set_subghzd_config(modulation, receiver_model.sampling_rate());
};
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
options_mode.set_selected_index(modulation, true);
receiver_model.enable();
}
void SubCarView::on_tick_second() {
@@ -176,6 +183,8 @@ const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
return "Fiat V0";
case FPC_BMWV0:
return "BMW V0";
/* case FPC_KIAV6:
return "Kia V6";*/
case FPC_Invalid:
default:
@@ -491,6 +500,13 @@ void SubCarRecentEntryDetailView::parseProtocol() {
btn = to_string_dec_uint(button);
}
/*if (entry_.sensorType == FPC_KIAV6) {
// not decrypted!
serial = 0;
btn = "?";
cnt = 0;
}*/
return;
}
+19 -13
View File
@@ -116,25 +116,27 @@ class SubCarView : public View {
4'000'000 /* sampling rate */,
ReceiverModel::Mode::AMAudio};
bool logging = false;
uint8_t modulation = 0;
app_settings::SettingsManager settings_{
"rx_subcar",
app_settings::Mode::RX,
{
{"log"sv, &logging},
{"modulationmode"sv, &modulation},
}};
SubCarRecentEntries recent{};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4},
};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -143,18 +145,22 @@ class SubCarView : public View {
SignalToken signal_token_tick_second{};
Button button_clear_list{
{0, 16, 7 * 8, 32},
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)},
"Clear"};
Checkbox check_log{
{10 * 8, 18},
{UI_POS_X(8), UI_POS_Y(1)},
3,
"Log",
true};
Labels labels{
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
};
ui::OptionsField options_mode{
{UI_POS_X(22), UI_POS_Y(1)},
3,
{{"AM", 0}, {"FM", 1}}};
static constexpr auto header_height = 3 * 16;
@@ -198,16 +204,16 @@ class SubCarRecentEntryDetailView : public View {
std::string btn = "";
uint32_t cnt = SD_NO_CNT;
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"};
Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"};
Console console{
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
{UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground},
};
Button button_done{
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV6DecoderStepReset = 0,
KiaV6DecoderStepWaitFirstHigh,
KiaV6DecoderStepCountPreamble,
KiaV6DecoderStepWaitLongHigh,
KiaV6DecoderStepData,
} KiaV6DecoderStep;
#define KIA_V6_XOR_MASK_LOW 0x84AF25FB
#define KIA_V6_XOR_MASK_HIGH 0x638766AB
class FProtoSubCarKiaV6 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV6() {
sensorType = FPC_KIAV6;
te_short = 200;
te_long = 400;
te_delta = 100;
min_count_bit_for_found = 144;
}
void feed(bool level, uint32_t duration) {
uint32_t uVar4, uVar5;
ManchesterEvent event;
bool data_bit;
uint8_t bit_count_inc;
uint32_t step_value;
switch (parser_step) {
case KiaV6DecoderStepReset: // case 0
if (level == 0) {
return;
}
if (DURATION_DIFF(duration, te_short) <
te_delta) {
parser_step = KiaV6DecoderStepWaitFirstHigh;
te_last = duration;
header_count = 0;
FProtoGeneral::manchester_advance(
manchester_state,
ManchesterEventReset,
&manchester_state,
NULL);
}
return;
case KiaV6DecoderStepWaitFirstHigh: { // case 1
if (level != 0) {
return;
}
uint32_t diff_short = DURATION_DIFF(duration, te_short);
uint32_t diff_long = DURATION_DIFF(duration, te_long);
uint32_t diff = (diff_long < diff_short) ? diff_long : diff_short;
if (diff_long < te_delta && diff_long < diff_short) {
if (header_count >= 0x259) { // 601 decimal
header_count = 0;
te_last = duration;
parser_step = KiaV6DecoderStepWaitLongHigh;
return;
}
}
if (diff >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
if (DURATION_DIFF(te_last, te_short) <
te_delta) {
te_last = duration;
header_count++;
return;
} else {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
case KiaV6DecoderStepWaitLongHigh: { // case 2
if (level == 0) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
uint32_t diff_long_check = DURATION_DIFF(duration, te_long);
uint32_t diff_short_check = DURATION_DIFF(duration, te_short);
if (diff_long_check >= te_delta) {
if (diff_short_check >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
if (DURATION_DIFF(te_last, te_long) >=
te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
decode_data = 0;
decode_count_bit = 0;
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(0);
subghz_protocol_blocks_add_bit(1);
data_part1_low = (uint32_t)(decode_data & 0xFFFFFFFF);
data_part1_high = (uint32_t)((decode_data >> 32) & 0xFFFFFFFF);
bit_count = decode_count_bit;
parser_step = KiaV6DecoderStepData;
return;
}
case KiaV6DecoderStepData: // case 3
if (DURATION_DIFF(duration, te_short) <
te_delta) {
event = (ManchesterEvent)((level & 0x7F) << 1);
goto manchester_process;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
event = (ManchesterEvent)(level ? 6 : 4);
goto manchester_process;
}
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
manchester_process:
if (FProtoGeneral::manchester_advance(
manchester_state, event, &manchester_state, &data_bit)) {
uVar4 = data_part1_low;
uVar5 = (uVar4 << 1) | (data_bit ? 1 : 0);
uint32_t carry = (uVar4 >> 31) & 1;
uVar4 = (data_part1_high << 1) | carry;
data_part1_low = uVar5;
data_part1_high = uVar4;
decode_data = ((uint64_t)uVar4 << 32) | uVar5;
bit_count_inc = bit_count + 1;
bit_count = bit_count_inc;
if (bit_count_inc == 0x40) {
// stored_part1_low = ~uVar5;
// stored_part1_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
} else if (bit_count_inc == 0x80) {
// stored_part2_low = ~uVar5;
// stored_part2_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
}
}
te_last = duration;
if (bit_count != min_count_bit_for_found) {
return;
}
data_count_bit = min_count_bit_for_found;
// data_part3 = ~((uint16_t)data_part1_low);
// kia_v6_decrypt(); --won't
decode_data = data_part1_low | ((uint64_t)data_part1_high << 32);
if (callback) {
callback(this);
}
data_part1_low = 0;
data_part1_high = 0;
bit_count = 0;
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
default:
return;
}
LAB_reset:
parser_step = step_value;
return;
}
uint8_t bit_count = 0;
uint16_t header_count = 0;
ManchesterState manchester_state = ManchesterStateMid1;
uint32_t data_part1_low = 0;
uint32_t data_part1_high = 0;
// uint32_t stored_part1_low = 0;
// uint32_t stored_part1_high = 0;
// uint32_t stored_part2_low = 0;
// uint32_t stored_part2_high = 0;
// uint16_t data_part3 = 0;
};
+1 -1
View File
@@ -76,7 +76,7 @@ class FProtoSubGhzDSecPlusV1 : public FProtoSubGhzDBase {
packet_accepted |= SECPLUS_V1_PACKET_2_ACCEPTED;
if (packet_accepted == (SECPLUS_V1_PACKET_1_ACCEPTED | SECPLUS_V1_PACKET_2_ACCEPTED)) {
// subghz_protocol_secplus_v1_decode(); // disabled doe to lack of flash
// subghz_protocol_secplus_v1_decode(); // disabled due to lack of flash
// controller
// uint32_t fixed = (data >> 32) & 0xFFFFFFFF;
// cnt = data & 0xFFFFFFFF;
@@ -21,6 +21,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
#include "c-ford_v0.hpp"
#include "c-fiat_v0.hpp"
#include "c-bmw_v0.hpp"
// #include "c-kia_v6.hpp"
#ifndef __FPROTO_PROTOLISTCAR_H__
#define __FPROTO_PROTOLISTCAR_H__
@@ -42,6 +43,7 @@ class SubCarProtos : public FProtoListGeneral {
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
// protos[FPC_KIAV6] = new FProtoSubCarKiaV6(); //-- disabled, due to whole encrypted.
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
+2 -3
View File
@@ -8,9 +8,6 @@ These values must be present on the protocol's constructor, like FProtoWeatherAc
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
*/
#define FPM_AM 0
#define FPM_FM 1
enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_Invalid = 0,
FPC_SUZUKI = 1,
@@ -24,6 +21,8 @@ enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_FORDV0 = 9,
FPC_FIATV0 = 10,
FPC_BMWV0 = 11,
// FPC_KIAV6 = 12, //disabled, due to whole encrypted.
// FPC_PSA = 13, // IS whole encrypted
FPC_COUNT
};
+99 -126
View File
@@ -25,14 +25,6 @@
#include "portapack_shared_memory.hpp"
#include "event_m4.hpp"
static inline int get_quadrant(int16_t i, int16_t q) {
if (i >= 0) {
return (q >= 0) ? 0 : 3;
} else {
return (q >= 0) ? 1 : 2;
}
}
void SubCarProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
@@ -46,10 +38,6 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples)
feed_channel_stats(decim_1_out);
// for fm
const int32_t DC_ALPHA = 5; // Auto-centering speed
int32_t buffer_rotation_sum = 0;
for (size_t i = 0; i < decim_1_out.count; i++) {
// am
threshold = (low_estimate + high_estimate) / 2;
@@ -57,126 +45,103 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
int16_t re = decim_1_out.p[i].real();
int16_t im = decim_1_out.p[i].imag();
uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im);
mag = (mag >> 10);
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
if (modulation == 0) {
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
sig_state = STATE_GAP_START;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
numg = 0;
sig_state = STATE_GAP_START;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
}
if (modulation == 1) {
int32_t discrim = ((int32_t)im * fm_state.last_re) - ((int32_t)re * fm_state.last_im);
fm_state.last_re = re;
fm_state.last_im = im;
fm_state.smoothed_discrim += (discrim - fm_state.smoothed_discrim) >> 4;
// --- FM Part (Simple 2-FSK) ---
if (mag > (threshold / 2)) {
const int32_t fm_hysteresis = 2000;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_discrim > fm_hysteresis) {
new_level = true;
} else if (fm_state.smoothed_discrim < -fm_hysteresis) {
new_level = false;
}
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
int32_t duration_us = (fm_state.buffer_count * nsPerDecSamp) / 1000;
if (duration_us > 15) {
if (protoList) protoList->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
fm_state.buffer_count = 0;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
// fm part: -- NOT WORKING!!!! TODO FIX. AI code ;)
int current_quad = get_quadrant(re, im);
// Calculate Step (Current - Previous)
int diff = current_quad - fm_state.prev_quad;
// Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3)
// 3 -> 0 should be +1 (CCW)
// 0 -> 3 should be -1 (CW)
if (diff == -3)
diff = 1;
else if (diff == 3)
diff = -1;
// Update History
fm_state.prev_quad = current_quad;
// Accumulate Rotation
buffer_rotation_sum += diff;
}
// fm finish:
// 3. AUTO-CENTERING (DC BLOCKER)
// Even with quadrant counting, "drift" (hand effect) makes the wheel spin
// faster or slower. We need to subtract the average speed.
// Update our "Average Speed" estimate
// Note: buffer_rotation_sum is roughly proportional to frequency.
fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA;
// Remove the drift
int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset;
// 4. LOW PASS FILTER
const int32_t LPF_ALPHA = 4;
fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA;
// 5. DECISION LOGIC
// Threshold is small now because we are counting quadrant steps.
// Max steps per buffer (256 samples) is 256.
// Typical FSK deviation might give you +/- 10 to 50 steps per buffer.
const int32_t THRESHOLD = 3;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_error > THRESHOLD) {
new_level = true;
} else if (fm_state.smoothed_error < -THRESHOLD) {
new_level = false;
}
// 6. TIMING OUTPUT
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
// Output pulse duration
int32_t duration_us = fm_state.buffer_count * 512;
if (duration_us > 250) {
if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
}
@@ -189,6 +154,14 @@ void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) {
// constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused
// constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused
if (modulation != message.modulation) {
// reload protos to reset them all
if (protoList) {
delete protoList;
}
protoList = new SubCarProtos();
}
modulation = message.modulation;
baseband_fs = message.sampling_rate;
baseband_thread.set_sampling_rate(baseband_fs);
nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8
+8 -8
View File
@@ -56,8 +56,8 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t low_estimate = 100;
uint32_t high_estimate = 12000;
uint32_t min_high_level = 10;
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 0; // will be set later by configure message
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 4'000'000; // will be set later by configure message
uint32_t nsPerDecSamp = 0;
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
@@ -74,20 +74,20 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t threshold = 0x0630;
bool currentHiLow = false;
bool configured{false};
uint8_t mode = 0; // 0 = am, 1 = fm
// fm part:
struct DemodFMState {
int prev_quad = 0; // Stores 0, 1, 2, or 3
int32_t dc_offset = 0;
int32_t smoothed_error = 0;
bool current_logic_level = false;
uint32_t buffer_count = 0;
int16_t last_re = 0; // Store previous Real sample
int16_t last_im = 0;
int32_t smoothed_discrim = 0;
};
DemodFMState fm_state{};
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual)
uint8_t modulation = 0; // 0 am, 1 fm
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
void configure(const SubGhzFPRxConfigureMessage& message);
/* NB: Threads should be the last members in the class definition. */