auto-sync
This commit is contained in:
parent
92dde1e493
commit
77a85e8166
|
@ -8,5 +8,5 @@ import java.util.Collection;
|
||||||
*/
|
*/
|
||||||
public interface FuelAutoLogic {
|
public interface FuelAutoLogic {
|
||||||
// void MainWindow::calckGBC(double STEP)
|
// void MainWindow::calckGBC(double STEP)
|
||||||
FuelAutoTune.Result process(boolean smooth, Collection<FuelAutoTune.stDataOnline> dataECU, double STEP, double targetAFR, float[][] kgbcINIT);
|
Result process(boolean smooth, Collection<stDataOnline> dataECU, double STEP, double targetAFR, float[][] kgbcINIT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ public enum FuelAutoTune implements FuelAutoLogic {
|
||||||
// todo: eliminate this
|
// todo: eliminate this
|
||||||
// Fields.FUEL_RPM_COUNT
|
// Fields.FUEL_RPM_COUNT
|
||||||
// Fields.FUEL_LOAD_COUNT
|
// Fields.FUEL_LOAD_COUNT
|
||||||
private static final int SIZE = 16;
|
public static final int SIZE = 16;
|
||||||
|
|
||||||
private static boolean isLogEnabled() {
|
private static boolean isLogEnabled() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -23,70 +23,6 @@ public enum FuelAutoTune implements FuelAutoLogic {
|
||||||
// private static final int TEMP_CORR = 39;
|
// private static final int TEMP_CORR = 39;
|
||||||
|
|
||||||
|
|
||||||
public static class stDataOnline {
|
|
||||||
public final double AFR;
|
|
||||||
private final int rpm;
|
|
||||||
private final double engineLoad;
|
|
||||||
int rpmIndex;
|
|
||||||
int engineLoadIndex;
|
|
||||||
|
|
||||||
public stDataOnline(double AFR, int rpmIndex, int engineLoadIndex, int rpm, double engineLoad) {
|
|
||||||
this.rpm = rpm;
|
|
||||||
this.engineLoad = engineLoad;
|
|
||||||
if (rpmIndex < 0 || rpmIndex >= Fields.FUEL_RPM_COUNT)
|
|
||||||
throw new IllegalStateException("rpmIndex " + rpmIndex);
|
|
||||||
if (engineLoadIndex < 0 || engineLoadIndex >= Fields.FUEL_LOAD_COUNT)
|
|
||||||
throw new IllegalStateException("engineLoadIndex " + engineLoadIndex);
|
|
||||||
this.AFR = AFR;
|
|
||||||
this.rpmIndex = rpmIndex;
|
|
||||||
this.engineLoadIndex = engineLoadIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static stDataOnline valueOf(double AFR, int rpm, double engineLoad) {
|
|
||||||
int rpmIndex = (int) (rpm / 7000.0 * SIZE);
|
|
||||||
if (rpmIndex < 0 || rpmIndex >= Fields.FUEL_RPM_COUNT)
|
|
||||||
return null;
|
|
||||||
int engineLoadIndex = (int) (engineLoad / 120.0 * SIZE);
|
|
||||||
return new stDataOnline(AFR, rpmIndex, engineLoadIndex, rpm, engineLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
int getRpmIndex() {
|
|
||||||
return rpmIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getEngineLoadIndex() {
|
|
||||||
return (int) engineLoadIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int PRESS_RT_32() {
|
|
||||||
return getEngineLoadIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RPM_RT_32() {
|
|
||||||
return getRpmIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRpm() {
|
|
||||||
return rpm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getEngineLoad() {
|
|
||||||
return engineLoad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Result {
|
|
||||||
private final float[][] kgbcRES;
|
|
||||||
|
|
||||||
public Result(float[][] kgbcRES) {
|
|
||||||
this.kgbcRES = kgbcRES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float[][] getKgbcRES() {
|
|
||||||
return kgbcRES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// void MainWindow::calckGBC(double STEP)
|
// void MainWindow::calckGBC(double STEP)
|
||||||
@Override
|
@Override
|
||||||
public Result process(boolean smooth, Collection<stDataOnline> dataECU, double STEP, double targetAFR, float[][] kgbcINIT) {
|
public Result process(boolean smooth, Collection<stDataOnline> dataECU, double STEP, double targetAFR, float[][] kgbcINIT) {
|
||||||
|
|
|
@ -1,17 +1,48 @@
|
||||||
package com.rusefi.autotune;
|
package com.rusefi.autotune;
|
||||||
|
|
||||||
|
import com.rusefi.config.Fields;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (c) Andrey Belomutskiy 2013-2016
|
* (c) Andrey Belomutskiy 2013-2016
|
||||||
2/18/2016.
|
* 2/18/2016.
|
||||||
*/
|
*/
|
||||||
public enum FuelAutoTune2 implements FuelAutoLogic {
|
public enum FuelAutoTune2 implements FuelAutoLogic {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|
||||||
|
private static final int SIZE = 16;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FuelAutoTune.Result process(boolean smooth, Collection<FuelAutoTune.stDataOnline> dataECU, double STEP, double targetAFR, float[][] kgbcINIT) {
|
public Result process(boolean smooth, Collection<stDataOnline> dataECU, double GRAD, double targetAFR, float[][] VEcur) {
|
||||||
return null;
|
float result[][] = new float[SIZE][SIZE];
|
||||||
|
|
||||||
|
// proverka na statichnost' rezhimnoy tochki
|
||||||
|
boolean fl_static = true;
|
||||||
|
for (stDataOnline dataPoint : dataECU) {
|
||||||
|
// TODO
|
||||||
|
// proverka idet po trem poslednim dannym v dataECU
|
||||||
|
// proverka po rpmIndex
|
||||||
|
|
||||||
|
// proverka po engineLoadIndex
|
||||||
|
|
||||||
|
// esli tochka ne statichna to fl_static = false
|
||||||
|
}
|
||||||
|
if (!fl_static)
|
||||||
|
return null;
|
||||||
|
// end
|
||||||
|
stDataOnline s = dataECU.iterator().next();
|
||||||
|
double delta = (s.AFR - targetAFR) / targetAFR; // privedennoe otklonenie po toplivu
|
||||||
|
|
||||||
|
|
||||||
|
for (int r = 0; r < SIZE; r++) { //rpmIndex
|
||||||
|
for (int e = 0; e < SIZE; e++) { //engineLoadIndex
|
||||||
|
result[r][e] = (float) (VEcur[r][e] + VEcur[r][e] * delta * GRAD / Math.min(Math.max(Math.abs(s.getEngineLoadIndex() - e), Math.abs(s.getRpmIndex() - r)), 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new Result(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.rusefi.autotune;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (c) Andrey Belomutskiy 2013-2016
|
||||||
|
* 2/23/2016.
|
||||||
|
*/
|
||||||
|
public class Result {
|
||||||
|
private final float[][] kgbcRES;
|
||||||
|
|
||||||
|
public Result(float[][] kgbcRES) {
|
||||||
|
this.kgbcRES = kgbcRES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[][] getKgbcRES() {
|
||||||
|
return kgbcRES;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.rusefi.autotune;
|
||||||
|
|
||||||
|
import com.rusefi.config.Fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (c) Andrey Belomutskiy 2013-2016
|
||||||
|
* 2/23/2016.
|
||||||
|
*/
|
||||||
|
public class stDataOnline {
|
||||||
|
public final double AFR;
|
||||||
|
private final int rpm;
|
||||||
|
private final double engineLoad;
|
||||||
|
int rpmIndex;
|
||||||
|
int engineLoadIndex;
|
||||||
|
|
||||||
|
public stDataOnline(double AFR, int rpmIndex, int engineLoadIndex, int rpm, double engineLoad) {
|
||||||
|
this.rpm = rpm;
|
||||||
|
this.engineLoad = engineLoad;
|
||||||
|
if (rpmIndex < 0 || rpmIndex >= Fields.FUEL_RPM_COUNT)
|
||||||
|
throw new IllegalStateException("rpmIndex " + rpmIndex);
|
||||||
|
if (engineLoadIndex < 0 || engineLoadIndex >= Fields.FUEL_LOAD_COUNT)
|
||||||
|
throw new IllegalStateException("engineLoadIndex " + engineLoadIndex);
|
||||||
|
this.AFR = AFR;
|
||||||
|
this.rpmIndex = rpmIndex;
|
||||||
|
this.engineLoadIndex = engineLoadIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static stDataOnline valueOf(double AFR, int rpm, double engineLoad) {
|
||||||
|
int rpmIndex = (int) (rpm / 7000.0 * FuelAutoTune.SIZE);
|
||||||
|
if (rpmIndex < 0 || rpmIndex >= Fields.FUEL_RPM_COUNT)
|
||||||
|
return null;
|
||||||
|
int engineLoadIndex = (int) (engineLoad / 120.0 * FuelAutoTune.SIZE);
|
||||||
|
return new stDataOnline(AFR, rpmIndex, engineLoadIndex, rpm, engineLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getRpmIndex() {
|
||||||
|
return rpmIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEngineLoadIndex() {
|
||||||
|
return (int) engineLoadIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int PRESS_RT_32() {
|
||||||
|
return getEngineLoadIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RPM_RT_32() {
|
||||||
|
return getRpmIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRpm() {
|
||||||
|
return rpm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getEngineLoad() {
|
||||||
|
return engineLoad;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.rusefi.autotune.test;
|
||||||
|
|
||||||
|
import com.rusefi.autotune.FuelAutoTune2;
|
||||||
|
import com.rusefi.autotune.Result;
|
||||||
|
import com.rusefi.autotune.stDataOnline;
|
||||||
|
import com.rusefi.config.Fields;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2/23/2016
|
||||||
|
* (c) Andrey Belomutskiy 2013-2016
|
||||||
|
*/
|
||||||
|
public class FuelAutoTune2Test {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoTune() {
|
||||||
|
List<stDataOnline> dataPoints = new ArrayList<>();
|
||||||
|
dataPoints.add(stDataOnline.valueOf(13, 1200, 80));
|
||||||
|
|
||||||
|
{
|
||||||
|
Result r = FuelAutoTune2.INSTANCE.process(false, dataPoints, 0.1, 13, createVeTable());
|
||||||
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataPoints.add(stDataOnline.valueOf(13, 1200, 80));
|
||||||
|
dataPoints.add(stDataOnline.valueOf(14, 1300, 60));
|
||||||
|
dataPoints.add(stDataOnline.valueOf(15, 1400, 70));
|
||||||
|
dataPoints.add(stDataOnline.valueOf(16, 1500, 90));
|
||||||
|
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
dataPoints.add(stDataOnline.valueOf(16, 1500 + i, 90));
|
||||||
|
|
||||||
|
{
|
||||||
|
Result r = FuelAutoTune2.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
||||||
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
dataPoints.add(stDataOnline.valueOf(15, 1500 + i, 90));
|
||||||
|
|
||||||
|
{
|
||||||
|
Result r = FuelAutoTune2.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
||||||
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// todo: validate results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this method prints all values which do not equal default value
|
||||||
|
*/
|
||||||
|
private static void printNotDefault(float[][] array, double defaultValue) {
|
||||||
|
for (int i = 0; i < array.length; i++) {
|
||||||
|
printNotDefault(array[i], i, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printNotDefault(float[] array, int index, double defaultValue) {
|
||||||
|
for (int i = 0; i < array.length; i++) {
|
||||||
|
if (array[i] != defaultValue)
|
||||||
|
System.out.println("Found value: " + index + " " + i + ": " + array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[][] createVeTable() {
|
||||||
|
float kgbcINIT[][] = new float[Fields.FUEL_LOAD_COUNT][Fields.FUEL_RPM_COUNT];
|
||||||
|
for (int engineLoadIndex = 0; engineLoadIndex < Fields.FUEL_LOAD_COUNT; engineLoadIndex++) {
|
||||||
|
for (int rpmIndex = 0; rpmIndex < Fields.FUEL_RPM_COUNT; rpmIndex++) {
|
||||||
|
kgbcINIT[engineLoadIndex][rpmIndex] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kgbcINIT;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package com.rusefi.autotune.test;
|
package com.rusefi.autotune.test;
|
||||||
|
|
||||||
import com.rusefi.autotune.FuelAutoTune;
|
import com.rusefi.autotune.FuelAutoTune;
|
||||||
|
import com.rusefi.autotune.Result;
|
||||||
|
import com.rusefi.autotune.stDataOnline;
|
||||||
import com.rusefi.config.Fields;
|
import com.rusefi.config.Fields;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -15,32 +17,32 @@ public class FuelAutoTuneTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAutoTune() {
|
public void testAutoTune() {
|
||||||
List<FuelAutoTune.stDataOnline> dataPoints = new ArrayList<>();
|
List<stDataOnline> dataPoints = new ArrayList<>();
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(13, 1200, 80));
|
dataPoints.add(stDataOnline.valueOf(13, 1200, 80));
|
||||||
|
|
||||||
{
|
{
|
||||||
FuelAutoTune.Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.1, 13, createVeTable());
|
Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.1, 13, createVeTable());
|
||||||
printNotDefault(r.getKgbcRES(), 1);
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(13, 1200, 80));
|
dataPoints.add(stDataOnline.valueOf(13, 1200, 80));
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(14, 1300, 60));
|
dataPoints.add(stDataOnline.valueOf(14, 1300, 60));
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(15, 1400, 70));
|
dataPoints.add(stDataOnline.valueOf(15, 1400, 70));
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(16, 1500, 90));
|
dataPoints.add(stDataOnline.valueOf(16, 1500, 90));
|
||||||
|
|
||||||
for (int i = 0; i < 2000; i++)
|
for (int i = 0; i < 2000; i++)
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(16, 1500 + i, 90));
|
dataPoints.add(stDataOnline.valueOf(16, 1500 + i, 90));
|
||||||
|
|
||||||
{
|
{
|
||||||
FuelAutoTune.Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
||||||
printNotDefault(r.getKgbcRES(), 1);
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 2000; i++)
|
for (int i = 0; i < 2000; i++)
|
||||||
dataPoints.add(FuelAutoTune.stDataOnline.valueOf(15, 1500 + i, 90));
|
dataPoints.add(stDataOnline.valueOf(15, 1500 + i, 90));
|
||||||
|
|
||||||
{
|
{
|
||||||
FuelAutoTune.Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
Result r = FuelAutoTune.INSTANCE.process(false, dataPoints, 0.01, 13, createVeTable());
|
||||||
printNotDefault(r.getKgbcRES(), 1);
|
printNotDefault(r.getKgbcRES(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import com.rusefi.ConfigurationImage;
|
||||||
import com.rusefi.FileLog;
|
import com.rusefi.FileLog;
|
||||||
import com.rusefi.UploadChanges;
|
import com.rusefi.UploadChanges;
|
||||||
import com.rusefi.autotune.FuelAutoTune;
|
import com.rusefi.autotune.FuelAutoTune;
|
||||||
|
import com.rusefi.autotune.Result;
|
||||||
|
import com.rusefi.autotune.stDataOnline;
|
||||||
import com.rusefi.binaryprotocol.BinaryProtocol;
|
import com.rusefi.binaryprotocol.BinaryProtocol;
|
||||||
import com.rusefi.config.Fields;
|
import com.rusefi.config.Fields;
|
||||||
import com.rusefi.core.Sensor;
|
import com.rusefi.core.Sensor;
|
||||||
|
@ -26,7 +28,6 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.RunnableFuture;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (c) Andrey Belomutskiy 2013-2016
|
* (c) Andrey Belomutskiy 2013-2016
|
||||||
|
@ -190,7 +191,7 @@ public class FuelTunePane {
|
||||||
loadMap(veTable, Fields.VETABLE.getOffset());
|
loadMap(veTable, Fields.VETABLE.getOffset());
|
||||||
logMap("source", veTable);
|
logMap("source", veTable);
|
||||||
|
|
||||||
List<FuelAutoTune.stDataOnline> data = new ArrayList<>();
|
List<stDataOnline> data = new ArrayList<>();
|
||||||
synchronized (incomingDataPoints) {
|
synchronized (incomingDataPoints) {
|
||||||
for (FuelDataPoint point : incomingDataPoints)
|
for (FuelDataPoint point : incomingDataPoints)
|
||||||
data.add(point.asDataOnline());
|
data.add(point.asDataOnline());
|
||||||
|
@ -199,7 +200,7 @@ public class FuelTunePane {
|
||||||
writeDataPoints(data);
|
writeDataPoints(data);
|
||||||
|
|
||||||
// todo: move this away from AWT thread
|
// todo: move this away from AWT thread
|
||||||
FuelAutoTune.Result a = FuelAutoTune.INSTANCE.process(false, data, 0.1, 14.7, veTable);
|
Result a = FuelAutoTune.INSTANCE.process(false, data, 0.1, 14.7, veTable);
|
||||||
|
|
||||||
float[][] result = a.getKgbcRES();
|
float[][] result = a.getKgbcRES();
|
||||||
logMap("result", result);
|
logMap("result", result);
|
||||||
|
@ -209,14 +210,14 @@ public class FuelTunePane {
|
||||||
upload.setEnabled(true);
|
upload.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeDataPoints(List<FuelAutoTune.stDataOnline> data) {
|
private void writeDataPoints(List<stDataOnline> data) {
|
||||||
DataOutputStream dos = getTuneLogStream();
|
DataOutputStream dos = getTuneLogStream();
|
||||||
if (dos == null)
|
if (dos == null)
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
dos.writeBytes("Running with " + data.size() + " points\r\n");
|
dos.writeBytes("Running with " + data.size() + " points\r\n");
|
||||||
dos.writeBytes("AFR\tRPM\tload\r\n");
|
dos.writeBytes("AFR\tRPM\tload\r\n");
|
||||||
for (FuelAutoTune.stDataOnline point : data)
|
for (stDataOnline point : data)
|
||||||
dos.writeBytes(point.AFR +"\t" + point.getRpm() + "\t" + point.getEngineLoad() + "\r\n");
|
dos.writeBytes(point.AFR +"\t" + point.getRpm() + "\t" + point.getEngineLoad() + "\r\n");
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -357,8 +358,8 @@ public class FuelTunePane {
|
||||||
engineLoadIndex = Math.max(0, BinarySearch.binarySearch(engineLoad, veLoadBins));
|
engineLoadIndex = Math.max(0, BinarySearch.binarySearch(engineLoad, veLoadBins));
|
||||||
}
|
}
|
||||||
|
|
||||||
public FuelAutoTune.stDataOnline asDataOnline() {
|
public stDataOnline asDataOnline() {
|
||||||
return new FuelAutoTune.stDataOnline(afr, rpmIndex, engineLoadIndex, rpm, engineLoad);
|
return new stDataOnline(afr, rpmIndex, engineLoadIndex, rpm, engineLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue