- clean commit with knock_analyzer

This commit is contained in:
3er0.1ive 2024-05-04 13:27:32 +03:00
parent c72f3b4670
commit 2997d1cf5f
19 changed files with 964 additions and 6 deletions

View File

@ -1,3 +1,4 @@
SHORT_BOARD_NAME=proteus_f4
PROJECT_CPU=ARCH_STM32F4
USE_OPENBLT=yes
EXTRA_PARAMS= -DKNOCK_SPECTROGRAM=TRUE

View File

@ -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();

View File

@ -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;

View File

@ -11,6 +11,7 @@ enum class BigBufferUser {
ToothLogger,
PerfTrace,
TriggerScope,
KnockSpectrogram,
};
class BigBufferHandle {

View File

@ -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

View File

@ -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++;

View File

@ -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;

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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

View File

@ -5,3 +5,8 @@
void initSoftwareKnock();
void knockSamplingCallback(uint8_t cylinderIndex, efitick_t nowNt);
#if KNOCK_SPECTROGRAM
void knockSpectrogramEnable();
void knockSpectrogramDisable();
#endif

View File

@ -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

View File

@ -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 */

View File

@ -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

View File

@ -67,6 +67,7 @@ enum class PE : uint8_t {
GlobalLock,
GlobalUnlock,
SoftwareKnockProcess,
KnockAnalyzer,
LogTriggerTooth,
LuaTickFunction,
LuaOneCanRxFunction,

View File

@ -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 */

View File

@ -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());

View File

@ -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) {
}
}
}

View File

@ -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);
}
}
}