mirror of https://github.com/rusefi/rusefi.git
- clean commit with knock_analyzer
This commit is contained in:
parent
3167ca9769
commit
bdb997fe1a
|
@ -1,3 +1,4 @@
|
|||
SHORT_BOARD_NAME=proteus_f4
|
||||
PROJECT_CPU=ARCH_STM32F4
|
||||
USE_OPENBLT=yes
|
||||
EXTRA_PARAMS= -DKNOCK_SPECTROGRAM=TRUE
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
#include "bluetooth.h"
|
||||
#include "tunerstudio_io.h"
|
||||
#include "trigger_scope.h"
|
||||
#include "software_knock.h"
|
||||
#include "electronic_throttle.h"
|
||||
#include "live_data.h"
|
||||
|
||||
|
@ -391,6 +392,10 @@ static bool isKnownCommand(char command) {
|
|||
|| command == TS_GET_TEXT
|
||||
|| command == TS_CRC_CHECK_COMMAND
|
||||
|| command == TS_GET_FIRMWARE_VERSION
|
||||
#if KNOCK_SPECTROGRAM
|
||||
|| command == TS_KNOCK_SPECTROGRAM_ENABLE
|
||||
|| command == TS_KNOCK_SPECTROGRAM_DISABLE
|
||||
#endif
|
||||
|| command == TS_PERF_TRACE_BEGIN
|
||||
|| command == TS_PERF_TRACE_GET_BUFFER
|
||||
|| command == TS_GET_CONFIG_ERROR
|
||||
|
@ -833,6 +838,16 @@ int TunerStudio::handleCrcCommand(TsChannelBase* tsChannel, char *data, int inco
|
|||
sendErrorCode(tsChannel, TS_RESPONSE_OUT_OF_RANGE);
|
||||
break;
|
||||
#endif /* EFI_TOOTH_LOGGER */
|
||||
#if KNOCK_SPECTROGRAM
|
||||
case TS_KNOCK_SPECTROGRAM_ENABLE:
|
||||
knockSpectrogramEnable();
|
||||
sendOkResponse(tsChannel, TS_CRC);
|
||||
break;
|
||||
case TS_KNOCK_SPECTROGRAM_DISABLE:
|
||||
knockSpectrogramDisable();
|
||||
sendOkResponse(tsChannel, TS_CRC);
|
||||
break;
|
||||
#endif /* KNOCK_SPECTROGRAM */
|
||||
#if ENABLE_PERF_TRACE
|
||||
case TS_PERF_TRACE_BEGIN:
|
||||
perfTraceEnable();
|
||||
|
|
|
@ -262,6 +262,11 @@ void setDefaultBaseEngine() {
|
|||
engineConfiguration->tcuInputSpeedSensorTeeth = 1;
|
||||
engineConfiguration->issFilterReciprocal = 2;
|
||||
|
||||
//knock
|
||||
#if KNOCK_SPECTROGRAM
|
||||
engineConfiguration->enableKnockSpectrogram = false;
|
||||
#endif
|
||||
|
||||
// Check engine light
|
||||
#if EFI_PROD_CODE
|
||||
engineConfiguration->warningPeriod = 10;
|
||||
|
|
|
@ -11,6 +11,7 @@ enum class BigBufferUser {
|
|||
ToothLogger,
|
||||
PerfTrace,
|
||||
TriggerScope,
|
||||
KnockSpectrogram,
|
||||
};
|
||||
|
||||
class BigBufferHandle {
|
||||
|
|
|
@ -741,7 +741,7 @@ void initRealHardwareEngineController() {
|
|||
* UNUSED_SIZE constants.
|
||||
*/
|
||||
#ifndef RAM_UNUSED_SIZE
|
||||
#define RAM_UNUSED_SIZE 30000
|
||||
#define RAM_UNUSED_SIZE 18000
|
||||
#endif
|
||||
#ifndef CCM_UNUSED_SIZE
|
||||
#define CCM_UNUSED_SIZE 512
|
||||
|
|
|
@ -44,7 +44,7 @@ int getCylinderKnockBank(uint8_t cylinderNumber) {
|
|||
}
|
||||
}
|
||||
|
||||
bool KnockControllerBase::onKnockSenseCompleted(uint8_t cylinderNumber, float dbv, efitick_t lastKnockTime) {
|
||||
bool KnockControllerBase::onKnockSenseCompleted(uint8_t cylinderNumber, float dbv, float frequency, efitick_t lastKnockTime) {
|
||||
bool isKnock = dbv > m_knockThreshold;
|
||||
|
||||
// Per-cylinder peak detector
|
||||
|
@ -53,6 +53,7 @@ bool KnockControllerBase::onKnockSenseCompleted(uint8_t cylinderNumber, float db
|
|||
|
||||
// All-cylinders peak detector
|
||||
m_knockLevel = allCylinderPeakDetector.detect(dbv, lastKnockTime);
|
||||
m_knockFrequency = frequency;
|
||||
|
||||
if (isKnock) {
|
||||
m_knockCount++;
|
||||
|
|
|
@ -22,7 +22,7 @@ public:
|
|||
void onFastCallback() override;
|
||||
|
||||
// onKnockSenseCompleted is the callback from the knock sense driver to report a sensed knock level
|
||||
bool onKnockSenseCompleted(uint8_t cylinderNumber, float dbv, efitick_t lastKnockTime);
|
||||
bool onKnockSenseCompleted(uint8_t cylinderNumber, float dbv, float frequency, efitick_t lastKnockTime);
|
||||
|
||||
float getKnockRetard() const;
|
||||
uint32_t getKnockCount() const;
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <math.h>
|
||||
#include <vector>
|
||||
#include <complex>
|
||||
|
||||
namespace fft {
|
||||
|
||||
typedef float real_type;
|
||||
typedef std::complex<real_type> complex_type;
|
||||
|
||||
bool fft_adc_sample(float * w, float ratio, const adcsample_t* data_in, complex_type* data_out, const size_t size);
|
||||
bool fft(const real_type* data_in, complex_type* data_out, const size_t size);
|
||||
void fft_freq(real_type* freq, const size_t size, const size_t sampleFreq);
|
||||
void fft_amp(const complex_type* fft_data, real_type* amplitude, const size_t size);
|
||||
void fft_db(real_type* amplitude, const size_t size);
|
||||
|
||||
void rectwin(float * w, unsigned n);
|
||||
void hann(float * w, unsigned n, bool sflag);
|
||||
void hamming(float * w, unsigned n, bool sflag);
|
||||
void blackman(float * w, unsigned n, bool sflag);
|
||||
void blackmanharris(float * w, unsigned n, bool sflag);
|
||||
|
||||
float get_main_freq(float* amplitudes, float* frequencies);
|
||||
|
||||
}
|
||||
|
||||
#include "fft.hpp"
|
|
@ -0,0 +1,220 @@
|
|||
#pragma once
|
||||
|
||||
namespace fft {
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.1415926535897932
|
||||
#endif
|
||||
|
||||
|
||||
inline bool isPow(const size_t num)
|
||||
{
|
||||
return num && (!(num & (num - 1)));
|
||||
}
|
||||
|
||||
void rerrange(complex_type* data, const size_t num_elements)
|
||||
{
|
||||
size_t target_index = 0;
|
||||
size_t bit_mask;
|
||||
|
||||
complex_type buffer;
|
||||
for (size_t i = 0; i < num_elements; ++i)
|
||||
{
|
||||
if (target_index > i)
|
||||
{
|
||||
buffer = data[target_index];
|
||||
data[target_index] = data[i];
|
||||
data[i]= buffer;
|
||||
}
|
||||
|
||||
bit_mask = num_elements;
|
||||
|
||||
while (target_index & (bit_mask >>= 1))
|
||||
{
|
||||
target_index &= ~bit_mask;
|
||||
}
|
||||
|
||||
target_index |= bit_mask;
|
||||
}
|
||||
}
|
||||
|
||||
bool transform(complex_type* data, const size_t count)
|
||||
{
|
||||
double local_pi = -M_PI;
|
||||
|
||||
size_t next, match;
|
||||
real_type sine;
|
||||
real_type delta;
|
||||
complex_type mult, factor, product;
|
||||
|
||||
for (size_t i = 1; i < count; i <<= 1)
|
||||
{
|
||||
next = i << 1;
|
||||
delta = local_pi / i;
|
||||
sine = sin(0.5 * delta);
|
||||
|
||||
mult = complex_type(-2.0 * sine * sine, sin(delta));
|
||||
factor = 1.0;
|
||||
|
||||
for (size_t j = 0; j < i; ++j)
|
||||
{
|
||||
for (size_t k = j; k < count; k += next)
|
||||
{
|
||||
match = k + i;
|
||||
|
||||
product = data[match] * factor;
|
||||
data[match] = data[k] - product;
|
||||
data[k] += product;
|
||||
}
|
||||
|
||||
factor = mult * factor + factor;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ffti(complex_type* data, const size_t size)
|
||||
{
|
||||
if(!isPow(size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rerrange(data, size);
|
||||
|
||||
return transform(data, size);
|
||||
}
|
||||
|
||||
bool fft_adc_sample(float * w, float ratio, const adcsample_t* data_in, complex_type* data_out, const size_t size)
|
||||
{
|
||||
for(size_t i = 0; i < size; ++i) {
|
||||
float voltage = ratio * data_in[i];
|
||||
//float db = 10 * log10(voltage * voltage);
|
||||
//db = clampF(-100, db, 100);
|
||||
data_out[i] = complex_type(voltage * w[i], 0.0);
|
||||
}
|
||||
|
||||
return ffti(data_out, size);
|
||||
}
|
||||
|
||||
bool fft(const real_type* data_in, complex_type* data_out, const size_t size)
|
||||
{
|
||||
for(size_t i = 0; i < size; ++i) {
|
||||
data_out[i] = complex_type(data_in[i], 0.0);
|
||||
}
|
||||
|
||||
return ffti(data_out, size);
|
||||
}
|
||||
|
||||
void fft_freq(real_type* freq, const size_t size, const size_t sampleFreq)
|
||||
{
|
||||
for (size_t i = 0; i < size/2; i++)
|
||||
{
|
||||
freq[i] = ((real_type)i * sampleFreq) / size;
|
||||
}
|
||||
}
|
||||
|
||||
void fft_amp(const complex_type* fft_data, real_type* amplitude, const size_t size)
|
||||
{
|
||||
for (size_t i = 0; i < size/2; ++i)
|
||||
{
|
||||
amplitude[i] = abs(fft_data[i].imag());
|
||||
}
|
||||
}
|
||||
|
||||
void fft_db(real_type* amplitude, const size_t size)
|
||||
{
|
||||
for (size_t i = 0; i < size/2; ++i)
|
||||
{
|
||||
amplitude[i] = log10(amplitude[i] * amplitude[i]) * 10;
|
||||
}
|
||||
}
|
||||
|
||||
void cosine_window(float * w, unsigned n, const float * coeff, unsigned ncoeff, bool sflag)
|
||||
{
|
||||
if (n == 1)
|
||||
{
|
||||
w[0] = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const unsigned wlength = sflag ? (n - 1) : n;
|
||||
|
||||
for (unsigned i = 0; i < n; ++i)
|
||||
{
|
||||
float wi = 0.0;
|
||||
|
||||
for (unsigned j = 0; j < ncoeff; ++j)
|
||||
{
|
||||
wi += coeff[j] * cos(i * j * 2.0 * M_PI / wlength);
|
||||
}
|
||||
|
||||
w[i] = wi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rectwin(float * w, unsigned n)
|
||||
{
|
||||
for (unsigned i = 0; i < n; ++i)
|
||||
{
|
||||
w[i] = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
void hann(float * w, unsigned n, bool sflag)
|
||||
{
|
||||
const float coeff[2] = { 0.5, -0.5 };
|
||||
|
||||
cosine_window(w, n, coeff, sizeof(coeff) / sizeof(float), sflag);
|
||||
}
|
||||
|
||||
void hamming(float * w, unsigned n, bool sflag)
|
||||
{
|
||||
const float coeff[2] = { 0.54, -0.46 };
|
||||
cosine_window(w, n, coeff, sizeof(coeff) / sizeof(float), sflag);
|
||||
}
|
||||
|
||||
void blackman(float * w, unsigned n, bool sflag)
|
||||
{
|
||||
const float coeff[3] = { 0.42, -0.5, 0.08 };
|
||||
cosine_window(w, n, coeff, sizeof(coeff) / sizeof(float), sflag);
|
||||
}
|
||||
|
||||
void blackmanharris(float * w, unsigned n, bool sflag)
|
||||
{
|
||||
const float coeff[4] = { 0.35875, -0.48829, 0.14128, -0.01168 };
|
||||
cosine_window(w, n, coeff, sizeof(coeff) / sizeof(float), sflag);
|
||||
}
|
||||
|
||||
float get_main_freq(float* amplitudes, float* frequencies, size_t size)
|
||||
{
|
||||
float peaks_amp = -100.f;
|
||||
size_t peaks_index = 0;
|
||||
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
float amp = amplitudes[i];
|
||||
if(amp > peaks_amp) {
|
||||
peaks_amp = amp;
|
||||
peaks_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
float sum_amps = amplitudes[peaks_index - 2] +
|
||||
amplitudes[peaks_index - 1] +
|
||||
amplitudes[peaks_index] +
|
||||
amplitudes[peaks_index + 1] +
|
||||
amplitudes[peaks_index + 2];
|
||||
|
||||
float sum_wighted = amplitudes[peaks_index - 2] * frequencies[peaks_index - 2] +
|
||||
amplitudes[peaks_index - 1] * frequencies[peaks_index - 1] +
|
||||
amplitudes[peaks_index] * frequencies[peaks_index] +
|
||||
amplitudes[peaks_index + 1] * frequencies[peaks_index + 1] +
|
||||
amplitudes[peaks_index + 2] * frequencies[peaks_index + 2];
|
||||
|
||||
float mean_freq = sum_wighted / sum_amps;
|
||||
return mean_freq;
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,27 @@
|
|||
#include "knock_config.h"
|
||||
#include "ch.hpp"
|
||||
|
||||
static NO_CACHE adcsample_t sampleBuffer[2000];
|
||||
#if KNOCK_SPECTROGRAM
|
||||
#include "development/knock_spectrogram.h"
|
||||
#include "fft/fft.h"
|
||||
|
||||
#define SIZE 512
|
||||
|
||||
struct SpectrogramData {
|
||||
fft::complex_type fftBuffer[SIZE];
|
||||
float frequencies[SIZE/2];
|
||||
float amplitudes[SIZE/2];
|
||||
float window[SIZE];
|
||||
};
|
||||
|
||||
static SpectrogramData spectrogramData0; // temporary use ram, will use big_buffer
|
||||
static SpectrogramData* spectrogramData = &spectrogramData0;
|
||||
static volatile bool enableKnockSpectrogram = false;
|
||||
//static BigBufferHandle buffer;
|
||||
#endif //KNOCK_SPECTROGRAM
|
||||
|
||||
|
||||
static NO_CACHE adcsample_t sampleBuffer[1800];
|
||||
static int8_t currentCylinderNumber = 0;
|
||||
static efitick_t lastKnockSampleTime = 0;
|
||||
static Biquad knockFilter;
|
||||
|
@ -154,6 +174,10 @@ void initSoftwareKnock() {
|
|||
knockFilter.configureBandpass(KNOCK_SAMPLE_RATE, 1000 * engineConfiguration->knockBandCustom, 3);
|
||||
adcStart(&KNOCK_ADC, nullptr);
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
engineConfiguration->enableKnockSpectrogram = false;
|
||||
#endif
|
||||
|
||||
// fun fact: we do not offer any ADC channel flexibility like we have for many other kinds of inputs
|
||||
efiSetPadMode("knock ch1", KNOCK_PIN_CH1, PAL_MODE_INPUT_ANALOG);
|
||||
#if KNOCK_HAS_CH2
|
||||
|
@ -168,6 +192,11 @@ static void processLastKnockEvent() {
|
|||
return;
|
||||
}
|
||||
|
||||
{
|
||||
chibios_rt::CriticalSectionLocker csl;
|
||||
enableKnockSpectrogram = engineConfiguration->enableKnockSpectrogram;
|
||||
}
|
||||
|
||||
float sumSq = 0;
|
||||
|
||||
// todo: reduce magic constants. engineConfiguration->adcVcc?
|
||||
|
@ -199,6 +228,26 @@ static void processLastKnockEvent() {
|
|||
// We're done with inspecting the buffer, another sample can be taken
|
||||
knockNeedsProcess = false;
|
||||
|
||||
float mainFreq = 0.f;
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
if(enableKnockSpectrogram) {
|
||||
//ScopePerf perf(PE::KnockAnalyzer);
|
||||
constexpr float ratio = 3.3f / 4095.0f;
|
||||
|
||||
fft::fft_adc_sample(spectrogramData->window, ratio, sampleBuffer, spectrogramData->fftBuffer, SIZE);
|
||||
fft::fft_freq(spectrogramData->frequencies, SIZE, KNOCK_SAMPLE_RATE); //samples per sec 218750
|
||||
fft::fft_amp(spectrogramData->fftBuffer, spectrogramData->amplitudes, SIZE);
|
||||
//fft::fft_db(spectrogramData->amplitudes, SIZE);
|
||||
|
||||
float mainFreq = fft::get_main_freq(spectrogramData->amplitudes, spectrogramData->frequencies, SIZE / 2);
|
||||
|
||||
knockSpectorgramAddLine(mainFreq, spectrogramData->amplitudes, 60); // [60] to 25207.5kHz for optimize data size
|
||||
|
||||
//engineConfiguration->knockBandCustom = mainFreq / 1000; // need save max amplitude of all
|
||||
}
|
||||
#endif
|
||||
|
||||
// mean of squares (not yet root)
|
||||
float meanSquares = sumSq / localCount;
|
||||
|
||||
|
@ -208,7 +257,7 @@ static void processLastKnockEvent() {
|
|||
// clamp to reasonable range
|
||||
db = clampF(-100, db, 100);
|
||||
|
||||
engine->module<KnockController>()->onKnockSenseCompleted(currentCylinderNumber, db, lastKnockTime);
|
||||
engine->module<KnockController>()->onKnockSenseCompleted(currentCylinderNumber, db, mainFreq, lastKnockTime);
|
||||
}
|
||||
|
||||
void KnockThread::ThreadTask() {
|
||||
|
@ -220,4 +269,32 @@ void KnockThread::ThreadTask() {
|
|||
}
|
||||
}
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
void knockSpectrogramEnable() {
|
||||
chibios_rt::CriticalSectionLocker csl;
|
||||
|
||||
// buffer is null, maybe need right release it
|
||||
// buffer = getBigBuffer(BigBufferUser::KnockSpectrogram);
|
||||
// if (!buffer) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// spectrogramData = buffer.get<SpectrogramData>();
|
||||
|
||||
fft::blackmanharris(spectrogramData->window, SIZE, true);
|
||||
|
||||
engineConfiguration->enableKnockSpectrogram = true;
|
||||
}
|
||||
|
||||
void knockSpectrogramDisable() {
|
||||
chibios_rt::CriticalSectionLocker csl;
|
||||
engineConfiguration->enableKnockSpectrogram = false;
|
||||
|
||||
//we're done with the buffer - let somebody else have it
|
||||
//buffer = {};
|
||||
//spectrogramData = nullptr;
|
||||
}
|
||||
|
||||
#endif /* KNOCK_SPECTROGRAM */
|
||||
|
||||
#endif // EFI_SOFTWARE_KNOCK
|
||||
|
|
|
@ -5,3 +5,8 @@
|
|||
|
||||
void initSoftwareKnock();
|
||||
void knockSamplingCallback(uint8_t cylinderIndex, efitick_t nowNt);
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
void knockSpectrogramEnable();
|
||||
void knockSpectrogramDisable();
|
||||
#endif
|
|
@ -9,4 +9,5 @@ DEV_SRC_CPP = $(DEVELOPMENT_DIR)/hw_layer/poten.cpp \
|
|||
$(DEVELOPMENT_DIR)/engine_emulator.cpp \
|
||||
$(DEVELOPMENT_DIR)/engine_sniffer.cpp \
|
||||
$(DEVELOPMENT_DIR)/logic_analyzer.cpp \
|
||||
$(DEVELOPMENT_DIR)/knock_spectrogram.cpp \
|
||||
$(DEVELOPMENT_DIR)/development/perf_trace.cpp
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* @file knock_spectrogram.cpp
|
||||
*
|
||||
* @date Feb 20, 2024
|
||||
* @author Alexey Ershov, (c) 2023-2024
|
||||
*/
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
|
||||
#include "knock_spectrogram.h"
|
||||
#include <climits>
|
||||
|
||||
#if EFI_TEXT_LOGGING
|
||||
static char PROTOCOL_KNOCK_SPECTROGRAMM_BUFFER[128] CCM_OPTIONAL;
|
||||
static Logging scLogging("knock_spectrogram", PROTOCOL_KNOCK_SPECTROGRAMM_BUFFER, sizeof(PROTOCOL_KNOCK_SPECTROGRAMM_BUFFER));
|
||||
|
||||
static char compressToByte(const float& v, const float& min, const float& max) {
|
||||
float vn = (v-min) / (max-min);
|
||||
int iv =int((float)256 * vn);
|
||||
|
||||
char compressed = (char)(iv-128);
|
||||
return compressed;
|
||||
}
|
||||
|
||||
void base64(Logging& l, const float* data, size_t size, const float& min, const float& max) {
|
||||
static constexpr char encTable[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
||||
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
|
||||
|
||||
size_t in_len = size;
|
||||
size_t out_len = 4 * ((in_len + 2) / 3);
|
||||
size_t i;
|
||||
|
||||
l.appendChar((char)out_len);
|
||||
|
||||
for (i = 0; in_len > 2 && i < in_len - 2; i += 3) {
|
||||
l.appendChar(encTable[(compressToByte(data[i], min, max) >> 2) & 0x3F]);
|
||||
l.appendChar(encTable[((compressToByte(data[i], min, max) & 0x3) << 4) |
|
||||
((int)(compressToByte(data[i + 1], min, max) & 0xF0) >> 4)]);
|
||||
l.appendChar(encTable[((compressToByte(data[i + 1], min, max) & 0xF) << 2) |
|
||||
((int)(compressToByte(data[i + 2], min, max) & 0xC0) >> 6)]);
|
||||
l.appendChar(encTable[compressToByte(data[i + 2], min, max) & 0x3F]);
|
||||
}
|
||||
if (i < in_len) {
|
||||
l.appendChar(encTable[(compressToByte(data[i], min, max) >> 2) & 0x3F]);
|
||||
|
||||
if (i == (in_len - 1)) {
|
||||
l.appendChar(encTable[((compressToByte(data[i], min, max) & 0x3) << 4)]);
|
||||
l.appendChar('=');
|
||||
} else {
|
||||
l.appendChar(encTable[((compressToByte(data[i], min, max) & 0x3) << 4) |
|
||||
((int)(compressToByte(data[i + 1], min, max) & 0xF0) >> 4)]);
|
||||
l.appendChar(encTable[((compressToByte(data[i + 1], min, max) & 0xF) << 2)]);
|
||||
}
|
||||
|
||||
l.appendChar('=');
|
||||
}
|
||||
}
|
||||
#endif /* EFI_TEXT_LOGGING */
|
||||
|
||||
void knockSpectorgramAddLine(float main_freq, float* data, size_t size) {
|
||||
#if EFI_TEXT_LOGGING
|
||||
if (scLogging.remainingSize() > size) {
|
||||
|
||||
float min = 99999999999;
|
||||
float max = -99999999999;
|
||||
for(size_t i = 0; i < size; ++i) {
|
||||
float v = data[i];
|
||||
if(v < min) {
|
||||
min = v;
|
||||
}
|
||||
|
||||
if(v > max) {
|
||||
max = v;
|
||||
}
|
||||
}
|
||||
|
||||
scLogging.reset();
|
||||
scLogging.appendPrintf(PROTOCOL_KNOCK_SPECTROGRAMM LOG_DELIMITER);
|
||||
|
||||
scLogging.appendFloat(main_freq, 2);
|
||||
scLogging.appendPrintf("*");
|
||||
|
||||
scLogging.appendChar((char)size);
|
||||
|
||||
base64(scLogging, data, size, min, max);
|
||||
|
||||
scLogging.append(LOG_DELIMITER);
|
||||
|
||||
scheduleLogging(&scLogging);
|
||||
}
|
||||
|
||||
#endif /* EFI_TEXT_LOGGING */
|
||||
}
|
||||
|
||||
#endif /* KNOCK_SPECTROGRAM */
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @file knock_spectrogram.h
|
||||
*
|
||||
* @date Feb 20, 2023
|
||||
* @author Alexey Ershov, (c) 2012-2023
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "global.h"
|
||||
|
||||
#if KNOCK_SPECTROGRAM
|
||||
void knockSpectorgramAddLine(float main_freq, float* data, size_t size);
|
||||
#endif
|
|
@ -67,6 +67,7 @@ enum class PE : uint8_t {
|
|||
GlobalLock,
|
||||
GlobalUnlock,
|
||||
SoftwareKnockProcess,
|
||||
KnockAnalyzer,
|
||||
LogTriggerTooth,
|
||||
LuaTickFunction,
|
||||
LuaOneCanRxFunction,
|
||||
|
|
|
@ -499,7 +499,7 @@ static msg_t hipThread(void *arg) {
|
|||
/* Check for correct cylinder/input */
|
||||
if (correctCylinder) {
|
||||
// TODO: convert knock level to dBv
|
||||
engine->module<KnockController>()->onKnockSenseCompleted(instance.cylinderNumber, knockVolts, instance.knockSampleTimestamp);
|
||||
engine->module<KnockController>()->onKnockSenseCompleted(instance.cylinderNumber, knockVolts, 0, instance.knockSampleTimestamp);
|
||||
|
||||
#if EFI_HIP_9011_DEBUG
|
||||
/* debug */
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.rusefi.ui.*;
|
|||
import com.rusefi.ui.console.MainFrame;
|
||||
import com.rusefi.ui.console.TabbedPanel;
|
||||
import com.rusefi.ui.engine.EngineSnifferPanel;
|
||||
import com.rusefi.ui.knock.KnockPane;
|
||||
import com.rusefi.ui.logview.LogViewer;
|
||||
import com.rusefi.ui.lua.LuaScriptPanel;
|
||||
import com.rusefi.ui.util.DefaultExceptionHandler;
|
||||
|
@ -116,6 +117,11 @@ public class ConsoleUI {
|
|||
tabbedPaneAdd("Sensor Sniffer", sensorSniffer.getPanel(), sensorSniffer.getTabSelectedListener());
|
||||
}
|
||||
|
||||
if (!linkManager.isLogViewer()) {
|
||||
KnockPane knockAnalyzer = new KnockPane(uiContext, getConfig().getRoot().getChild("knock_analyzer"));
|
||||
tabbedPaneAdd("Knock analyzer", knockAnalyzer.getPanel(), knockAnalyzer.getTabSelectedListener());
|
||||
}
|
||||
|
||||
// tabbedPane.addTab("LE controls", new FlexibleControls().getPanel());
|
||||
|
||||
// tabbedPane.addTab("ADC", new AdcPanel(new BooleanInputsModel()).createAdcPanel());
|
||||
|
|
|
@ -0,0 +1,439 @@
|
|||
package com.rusefi.ui.knock;
|
||||
|
||||
import com.devexperts.logging.Logging;
|
||||
import com.rusefi.FileLog;
|
||||
import com.rusefi.binaryprotocol.BinaryProtocol;
|
||||
import com.rusefi.config.generated.Fields;
|
||||
import com.rusefi.core.EngineState;
|
||||
import com.rusefi.core.preferences.storage.Node;
|
||||
import com.rusefi.tools.ConsoleTools;
|
||||
import com.rusefi.ui.RpmLabel;
|
||||
import com.rusefi.ui.RpmModel;
|
||||
import com.rusefi.ui.UIContext;
|
||||
import com.rusefi.ui.config.ConfigUiField;
|
||||
import com.rusefi.ui.util.UiUtils;
|
||||
import com.rusefi.ui.widgets.AnyCommand;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Random;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import static com.devexperts.logging.Logging.getLogging;
|
||||
import static com.rusefi.tools.ConsoleTools.startAndConnect;
|
||||
import static java.lang.Math.*;
|
||||
|
||||
/**
|
||||
* Date: 20/02/24
|
||||
* Aleksey Ershov, (c) 2013-2024
|
||||
*/
|
||||
public class KnockPane {
|
||||
private static final Logging log = getLogging(KnockPane.class);
|
||||
|
||||
private final KnockCanvas canvas = new KnockCanvas();
|
||||
|
||||
private final JPanel content = new JPanel(new BorderLayout());
|
||||
private final AnyCommand command;
|
||||
|
||||
|
||||
public class KnockKeListener extends KeyAdapter implements ActionListener {
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
|
||||
|
||||
}
|
||||
if (e.getKeyCode() == KeyEvent.VK_UP) {
|
||||
|
||||
}
|
||||
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
|
||||
|
||||
}
|
||||
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
//drawPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
public KnockPane(UIContext uiContext, Node config) {
|
||||
|
||||
uiContext.getLinkManager().getEngineState().registerStringValueAction(Fields.PROTOCOL_KNOCK_SPECTROGRAMM, new EngineState.ValueCallback<String>() {
|
||||
@Override
|
||||
public void onUpdate(String value) {
|
||||
canvas.processValues(value);
|
||||
canvas.repaint();
|
||||
}
|
||||
});
|
||||
|
||||
final JPanel upperPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
|
||||
|
||||
JButton enableButton = new JButton("start");
|
||||
enableButton.setMnemonic('s');
|
||||
|
||||
enableButton.addActionListener(new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
uiContext.getLinkManager().submit(() -> {
|
||||
//uiContext.getLinkManager().setCompositeLogicEnabled(false); // todo: use big_buffer
|
||||
BinaryProtocol binaryProtocol = uiContext.getLinkManager().getConnector().getBinaryProtocol();
|
||||
binaryProtocol.executeCommand(Fields.TS_KNOCK_SPECTROGRAM_ENABLE, "start knock analyzer");
|
||||
});
|
||||
}
|
||||
});
|
||||
upperPanel.add(enableButton);
|
||||
|
||||
JButton disableButton = new JButton("stop");
|
||||
disableButton.setMnemonic('s');
|
||||
disableButton.addActionListener(new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
uiContext.getLinkManager().submit(() -> {
|
||||
BinaryProtocol binaryProtocol = uiContext.getLinkManager().getConnector().getBinaryProtocol();
|
||||
binaryProtocol.executeCommand(Fields.TS_KNOCK_SPECTROGRAM_DISABLE, "stop knock analyzer");
|
||||
//uiContext.getLinkManager().setCompositeLogicEnabled(true); // todo: use big_buffer
|
||||
});
|
||||
}
|
||||
});
|
||||
upperPanel.add(disableButton);
|
||||
|
||||
|
||||
JButton saveImageButton = UiUtils.createSaveImageButton();
|
||||
upperPanel.add(saveImageButton);
|
||||
saveImageButton.addActionListener(e -> {
|
||||
int rpm = RpmModel.getInstance().getValue();
|
||||
String fileName = FileLog.getDate() + "_knock_" + rpm + "_spectrogram" + ".png";
|
||||
|
||||
UiUtils.saveImageWithPrompt(fileName, upperPanel, canvas);
|
||||
}
|
||||
);
|
||||
|
||||
upperPanel.add(new RpmLabel(uiContext,2).getContent());
|
||||
|
||||
command = AnyCommand.createField(uiContext, config, true, false);
|
||||
upperPanel.add(command.getContent());
|
||||
|
||||
content.add(upperPanel, BorderLayout.NORTH);
|
||||
|
||||
KnockKeListener l = new KnockKeListener();
|
||||
canvas.setFocusable(true);
|
||||
canvas.setFocusTraversalKeysEnabled(false);
|
||||
canvas.addKeyListener(l);
|
||||
canvas.setFocusable(true);
|
||||
canvas.setDoubleBuffered(true);
|
||||
content.add(canvas, BorderLayout.CENTER);
|
||||
|
||||
|
||||
final JPanel leftPanel = new KnockScale(this);
|
||||
leftPanel.setPreferredSize(new Dimension(150, 0));
|
||||
|
||||
content.add(leftPanel, BorderLayout.WEST);
|
||||
}
|
||||
|
||||
public KnockCanvas getCanvas() {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
public JComponent getPanel() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public ActionListener getTabSelectedListener() {
|
||||
return e -> command.requestFocus();
|
||||
}
|
||||
|
||||
public class KnockCanvas extends JComponent implements ComponentListener {
|
||||
|
||||
JComponent dd = this;
|
||||
//--------------------------------------
|
||||
|
||||
private BufferedImage bufferedImage;
|
||||
private Graphics2D bufferedGraphics;
|
||||
static int SPECTROGRAM_X_AXIS_SIZE = 1024;
|
||||
float[][] specrtogram;
|
||||
float mainFrequency = 0;
|
||||
Color[] colorspace;
|
||||
Color[] colors;
|
||||
float[] amplitudesInColorSpace;
|
||||
|
||||
int spectrogramYAxisSize;
|
||||
|
||||
public double yAxisHz[];
|
||||
|
||||
int currentIndexXAxis = 0;
|
||||
|
||||
private KnockCanvas() {
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable(){
|
||||
public void run() {
|
||||
dd.repaint();
|
||||
}
|
||||
});
|
||||
|
||||
bufferedImage = new BufferedImage(640,480, BufferedImage.TYPE_INT_RGB);
|
||||
bufferedGraphics = bufferedImage.createGraphics();
|
||||
this.addComponentListener(this);
|
||||
|
||||
//linear-gradient(to right, #000000, #290d1a, #490b32, #670353, #81007b, #a60085, #ca008b, #ef008f, #ff356b, #ff6947, #ff9a22, #ffc700);
|
||||
colorspace = new Color[] {
|
||||
Color.decode("#000000"),
|
||||
Color.decode("#290d1a"),
|
||||
Color.decode("#490b32"),
|
||||
Color.decode("#670353"),
|
||||
Color.decode("#81007b"),
|
||||
Color.decode("#a60085"),
|
||||
Color.decode("#ca008b"),
|
||||
Color.decode("#ef008f"),
|
||||
Color.decode("#ff356b"),
|
||||
Color.decode("#ff6947"),
|
||||
Color.decode("#ff9a22"),
|
||||
Color.decode("#ffc700"),
|
||||
};
|
||||
|
||||
// this values agreed with firmware sample rate(KNOCK_SAMPLE_RATE) and fft size(512)
|
||||
yAxisHz = new double[]{
|
||||
0, 427.246, 854.492, 1281.74, 1708.98, 2136.23, 2563.48, 2990.72, 3417.97, 3845.21,
|
||||
4272.46, 4699.71, 5126.95, 5554.2, 5981.45, 6408.69, 6835.94, 7263.18, 7690.43, 8117.68,
|
||||
8544.92, 8972.17, 9399.41, 9826.66, 10253.9, 10681.2, 11108.4, 11535.6, 11962.9, 12390.1,
|
||||
12817.4, 13244.6, 13671.9, 14099.1, 14526.4, 14953.6, 15380.9, 15808.1, 16235.4, 16662.6,
|
||||
17089.8, 17517.1, 17944.3, 18371.6, 18798.8, 19226.1, 19653.3, 20080.6, 20507.8, 20935.1,
|
||||
21362.3, 21789.6, 22216.8, 22644, 23071.3, 23498.5, 23925.8, 24353, 24780.3, 25207.5,
|
||||
};
|
||||
|
||||
spectrogramYAxisSize = yAxisHz.length;
|
||||
specrtogram = new float[SPECTROGRAM_X_AXIS_SIZE][spectrogramYAxisSize];
|
||||
colors = new Color[spectrogramYAxisSize];
|
||||
amplitudesInColorSpace = new float[spectrogramYAxisSize];
|
||||
}
|
||||
|
||||
private void processValues(String values) {
|
||||
|
||||
//log.info(values);
|
||||
|
||||
var first_split = values.split("\\*");
|
||||
var freq_str = first_split[0];
|
||||
|
||||
mainFrequency = Float.parseFloat(freq_str);
|
||||
|
||||
String compressed = first_split[1];
|
||||
char sizec = compressed.charAt(0);
|
||||
int size_data = (int)sizec;
|
||||
|
||||
char base64Size = compressed.charAt(1);
|
||||
int b64size = (int)base64Size;
|
||||
|
||||
String base64 = compressed.substring(2, b64size + 2);
|
||||
|
||||
byte[] data = Base64.getDecoder().decode(base64);
|
||||
|
||||
assert size_data == compressed.length() - 1;
|
||||
assert size_data == yAxisHz.length;
|
||||
|
||||
if(compressed.length() - 1 < spectrogramYAxisSize){
|
||||
log.error("data size error: " + compressed.length());
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < spectrogramYAxisSize; ++i) {
|
||||
|
||||
byte c = data[i];
|
||||
int k = c + 128;
|
||||
|
||||
specrtogram[currentIndexXAxis][i] = k;
|
||||
}
|
||||
|
||||
int width = bufferedImage.getWidth();
|
||||
int height = bufferedImage.getHeight();
|
||||
|
||||
float bx = (float)width / (float)SPECTROGRAM_X_AXIS_SIZE;
|
||||
|
||||
float min = Integer.MAX_VALUE;
|
||||
float max = 0;
|
||||
for(int x = 0; x < SPECTROGRAM_X_AXIS_SIZE; ++x) {
|
||||
for(int y = 0; y < spectrogramYAxisSize; ++y) {
|
||||
float value = specrtogram[x][y];
|
||||
if(value < min) {
|
||||
min = value;
|
||||
}
|
||||
|
||||
if(value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int y = 0; y < spectrogramYAxisSize; ++y)
|
||||
{
|
||||
float value = specrtogram[currentIndexXAxis][y];
|
||||
double lvalue = value;
|
||||
double lmin = min;
|
||||
double lmax = max;
|
||||
|
||||
double normalized = 0;
|
||||
if((lmax-lmin) != 0) {
|
||||
normalized = (lvalue-lmin)/(lmax-lmin);
|
||||
}
|
||||
|
||||
if(normalized > 1)
|
||||
{
|
||||
normalized = 1.0;
|
||||
}
|
||||
|
||||
if(normalized < 0)
|
||||
{
|
||||
normalized = 0.0;
|
||||
}
|
||||
|
||||
//int color_index = (int)((colorspace.length - 1) * (float)random()); //for test
|
||||
int color_index = (int)((colorspace.length-1) * normalized);
|
||||
|
||||
Color color = colorspace[color_index];
|
||||
|
||||
colors[(spectrogramYAxisSize-1) - y] = color;
|
||||
amplitudesInColorSpace[y] = ((float)y) / (float) spectrogramYAxisSize;
|
||||
}
|
||||
|
||||
LinearGradientPaint lgp = new LinearGradientPaint(
|
||||
new Point2D.Float(0, 0),
|
||||
new Point2D.Float(0, height),
|
||||
amplitudesInColorSpace,
|
||||
colors
|
||||
);
|
||||
|
||||
bufferedGraphics.setPaint(lgp);
|
||||
|
||||
bufferedGraphics.fillRect((int)(currentIndexXAxis * bx), 0, (int)bx, height);
|
||||
|
||||
//log.info(Arrays.toString(specrtogram[currentIndexXAxis]));
|
||||
|
||||
++currentIndexXAxis;
|
||||
|
||||
if(currentIndexXAxis >= SPECTROGRAM_X_AXIS_SIZE){
|
||||
currentIndexXAxis = 0;
|
||||
}
|
||||
}
|
||||
|
||||
double lerp(double start, double end, double t) {
|
||||
return start * (1 - t) + end * t;
|
||||
}
|
||||
|
||||
private static int searchHZ(double[] a, int fromIndex, int toIndex, double key) {
|
||||
int low = fromIndex;
|
||||
int high = toIndex - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
double midVal = a[mid];
|
||||
|
||||
if (midVal < key)
|
||||
low = mid + 1;
|
||||
else if (midVal > key)
|
||||
high = mid - 1;
|
||||
else
|
||||
return mid; // key found
|
||||
}
|
||||
return low; // key not found.
|
||||
}
|
||||
|
||||
int hzToYScreen(double hz, int screen_height) {
|
||||
|
||||
var near_hz_index = searchHZ(yAxisHz, 0, yAxisHz.length - 1, hz);
|
||||
|
||||
int a = near_hz_index-1;
|
||||
int b = near_hz_index;
|
||||
|
||||
if(a < 0 || b > yAxisHz.length - 1) {
|
||||
// out of bounds
|
||||
return -1;
|
||||
}
|
||||
|
||||
double a_value = yAxisHz[a];
|
||||
double b_value = yAxisHz[b];
|
||||
|
||||
double t = (hz - a_value) / (b_value - a_value);
|
||||
|
||||
double y_step = (double)screen_height / (double)yAxisHz.length;
|
||||
|
||||
double a_screen = (y_step * (a));
|
||||
double b_screen = (y_step * (b));
|
||||
|
||||
double y_screen = lerp(a_screen, b_screen,t);
|
||||
|
||||
return screen_height - (int)y_screen;
|
||||
}
|
||||
|
||||
float YScreenToHz(int y, int screen_height) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g) {
|
||||
super.paint(g);
|
||||
|
||||
Dimension size = getSize();
|
||||
|
||||
// flip buffers
|
||||
g.drawImage(bufferedImage, 0, 0, size.width, size.height,null);
|
||||
|
||||
int width = bufferedImage.getWidth();
|
||||
int height = bufferedImage.getHeight();
|
||||
|
||||
float bx = (float)width / (float)SPECTROGRAM_X_AXIS_SIZE;
|
||||
|
||||
g.setColor(Color.RED);
|
||||
int line = (int)(currentIndexXAxis * bx);
|
||||
g.drawLine(line, 0, line, height);
|
||||
|
||||
|
||||
for(int i = 0; i < yAxisHz.length; ++i) {
|
||||
|
||||
var y= hzToYScreen(yAxisHz[i], height);
|
||||
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, y, 30, 3);
|
||||
}
|
||||
|
||||
Font f = g.getFont();
|
||||
g.setFont(new Font(f.getName(), Font.BOLD, g.getFont().getSize() * 10));
|
||||
g.setColor(Color.RED);
|
||||
g.drawString(Float.valueOf(mainFrequency).toString() + " Hz", width/2, 100);
|
||||
g.setFont(f);
|
||||
|
||||
g.setColor(Color.WHITE);
|
||||
var yy = hzToYScreen(mainFrequency, height);
|
||||
g.fillRect(0, yy, width, 1);
|
||||
|
||||
//for test
|
||||
//var yy2 = hzToYScreen(8117.68, height);
|
||||
//g.fillRect(0, yy2, width, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentHidden(ComponentEvent e) {
|
||||
}
|
||||
@Override
|
||||
public void componentMoved(ComponentEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
bufferedImage = new BufferedImage(getWidth(),getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
bufferedGraphics = bufferedImage.createGraphics();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.rusefi.ui.knock;
|
||||
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class KnockScale extends JPanel {
|
||||
|
||||
double yAxisHz[];
|
||||
|
||||
KnockPane knockPane;
|
||||
public KnockScale(KnockPane pane) {
|
||||
super(new FlowLayout());
|
||||
|
||||
knockPane = pane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g) {
|
||||
super.paint(g);
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
|
||||
Dimension size = getSize();
|
||||
|
||||
Font f = g.getFont();
|
||||
g.setFont(new Font(f.getFontName(), Font.BOLD, 22));
|
||||
|
||||
for(int i = 0; i < knockPane.getCanvas().yAxisHz.length; ++i) {
|
||||
int dy = size.height / knockPane.getCanvas().yAxisHz.length;
|
||||
double hz = knockPane.getCanvas().yAxisHz[i];
|
||||
|
||||
int y = knockPane.getCanvas().hzToYScreen(hz, size.height);
|
||||
|
||||
g.setColor(Color.BLACK);
|
||||
|
||||
g.drawString(Integer.valueOf((int)hz).toString() + " Hz", getWidth() - 130, y + 6);
|
||||
|
||||
g.setColor(Color.BLACK);
|
||||
g2.fillRect(getWidth() - 30, y, getWidth(), 3);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue