mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-10-14 09:03:30 +02:00
Garmin protocol: move field encode/decode interface to the FieldDefinition
This allows for semantic subclassing the FieldDefinition. A FieldDefinitionTimestamp subclass is introduced as example
This commit is contained in:
parent
a12a638f61
commit
149e388265
@ -147,8 +147,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
|||||||
|
|
||||||
RecordData today = new RecordData(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
|
RecordData today = new RecordData(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
|
||||||
today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||||
today.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp));
|
today.setFieldByName("timestamp", weather.timestamp);
|
||||||
today.setFieldByName("observed_at_time", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp));
|
today.setFieldByName("observed_at_time", weather.timestamp);
|
||||||
today.setFieldByName("temperature", weather.currentTemp - 273.15);
|
today.setFieldByName("temperature", weather.currentTemp - 273.15);
|
||||||
today.setFieldByName("low_temperature", weather.todayMinTemp - 273.15);
|
today.setFieldByName("low_temperature", weather.todayMinTemp - 273.15);
|
||||||
today.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15);
|
today.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15);
|
||||||
@ -168,7 +168,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
|||||||
WeatherSpec.Hourly hourly = weather.hourly.get(hour);
|
WeatherSpec.Hourly hourly = weather.hourly.get(hour);
|
||||||
RecordData weatherHourlyForecast = new RecordData(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition());
|
RecordData weatherHourlyForecast = new RecordData(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition());
|
||||||
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||||
weatherHourlyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(hourly.timestamp));
|
weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp);
|
||||||
weatherHourlyForecast.setFieldByName("temperature", hourly.temp - 273.15);
|
weatherHourlyForecast.setFieldByName("temperature", hourly.temp - 273.15);
|
||||||
weatherHourlyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(hourly.conditionCode));
|
weatherHourlyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(hourly.conditionCode));
|
||||||
weatherHourlyForecast.setFieldByName("wind_direction", hourly.windDirection);
|
weatherHourlyForecast.setFieldByName("wind_direction", hourly.windDirection);
|
||||||
@ -184,7 +184,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
|||||||
//
|
//
|
||||||
RecordData todayDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
RecordData todayDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
||||||
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||||
todayDailyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp));
|
todayDailyForecast.setFieldByName("timestamp", weather.timestamp);
|
||||||
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp - 273.15);
|
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp - 273.15);
|
||||||
todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15);
|
todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15);
|
||||||
todayDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(weather.currentConditionCode));
|
todayDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(weather.currentConditionCode));
|
||||||
@ -199,7 +199,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
|||||||
int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed?
|
int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed?
|
||||||
RecordData weatherDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
RecordData weatherDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
||||||
weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||||
weatherDailyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp));
|
weatherDailyForecast.setFieldByName("timestamp", weather.timestamp);
|
||||||
weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp - 273.15);
|
weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp - 273.15);
|
||||||
weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp - 273.15);
|
weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp - 273.15);
|
||||||
weatherDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(daily.conditionCode));
|
weatherDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(daily.conditionCode));
|
||||||
|
@ -5,7 +5,7 @@ import org.threeten.bp.ZoneId;
|
|||||||
|
|
||||||
public class GarminTimeUtils {
|
public class GarminTimeUtils {
|
||||||
|
|
||||||
private static final int GARMIN_TIME_EPOCH = 631065600;
|
public static final int GARMIN_TIME_EPOCH = 631065600;
|
||||||
|
|
||||||
public static int unixTimeToGarminTimestamp(int unixTime) {
|
public static int unixTimeToGarminTimestamp(int unixTime) {
|
||||||
return unixTime - GARMIN_TIME_EPOCH;
|
return unixTime - GARMIN_TIME_EPOCH;
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||||
|
|
||||||
public class FieldDefinition {
|
public class FieldDefinition implements FieldInterface {
|
||||||
private final int localNumber;
|
private final int localNumber;
|
||||||
private final int size;
|
private final int size;
|
||||||
private final BaseType baseType;
|
protected final BaseType baseType;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final int scale;
|
protected final int scale;
|
||||||
private final int offset;
|
protected final int offset;
|
||||||
|
|
||||||
public FieldDefinition(int localNumber, int size, BaseType baseType, String name, int scale, int offset) {
|
public FieldDefinition(int localNumber, int size, BaseType baseType, String name, int scale, int offset) {
|
||||||
this.localNumber = localNumber;
|
this.localNumber = localNumber;
|
||||||
@ -70,4 +72,18 @@ public class FieldDefinition {
|
|||||||
writer.writeByte(baseType.getIdentifier());
|
writer.writeByte(baseType.getIdentifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object decode(ByteBuffer byteBuffer) {
|
||||||
|
return baseType.decode(byteBuffer, scale, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||||
|
baseType.encode(byteBuffer, o, scale, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidate(ByteBuffer byteBuffer) {
|
||||||
|
baseType.invalidate(byteBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public interface FieldInterface {
|
||||||
|
Object decode(ByteBuffer byteBuffer);
|
||||||
|
|
||||||
|
void encode(ByteBuffer byteBuffer, Object o);
|
||||||
|
|
||||||
|
void invalidate(ByteBuffer byteBuffer);
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,7 @@ import java.nio.ByteOrder;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
|
||||||
|
|
||||||
public enum GlobalDefinitionsEnum {
|
public enum GlobalDefinitionsEnum {
|
||||||
TODAY_WEATHER_CONDITIONS(MesgType.TODAY_WEATHER_CONDITIONS, new RecordDefinition(
|
TODAY_WEATHER_CONDITIONS(MesgType.TODAY_WEATHER_CONDITIONS, new RecordDefinition(
|
||||||
@ -13,8 +14,8 @@ public enum GlobalDefinitionsEnum {
|
|||||||
ByteOrder.BIG_ENDIAN,
|
ByteOrder.BIG_ENDIAN,
|
||||||
MesgType.TODAY_WEATHER_CONDITIONS,
|
MesgType.TODAY_WEATHER_CONDITIONS,
|
||||||
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
||||||
new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"),
|
new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"),
|
||||||
new FieldDefinition(9, 4, BaseType.UINT32, "observed_at_time"),
|
new FieldDefinitionTimestamp(9, 4, BaseType.UINT32, "observed_at_time"),
|
||||||
new FieldDefinition(1, 1, BaseType.SINT8, "temperature"),
|
new FieldDefinition(1, 1, BaseType.SINT8, "temperature"),
|
||||||
new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"),
|
new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"),
|
||||||
new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"),
|
new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"),
|
||||||
@ -33,7 +34,7 @@ public enum GlobalDefinitionsEnum {
|
|||||||
ByteOrder.BIG_ENDIAN,
|
ByteOrder.BIG_ENDIAN,
|
||||||
MesgType.HOURLY_WEATHER_FORECAST,
|
MesgType.HOURLY_WEATHER_FORECAST,
|
||||||
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
||||||
new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"),
|
new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"),
|
||||||
new FieldDefinition(1, 1, BaseType.SINT8, "temperature"),
|
new FieldDefinition(1, 1, BaseType.SINT8, "temperature"),
|
||||||
new FieldDefinition(2, 1, BaseType.ENUM, "condition"),
|
new FieldDefinition(2, 1, BaseType.ENUM, "condition"),
|
||||||
new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"),
|
new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"),
|
||||||
@ -49,7 +50,7 @@ public enum GlobalDefinitionsEnum {
|
|||||||
ByteOrder.BIG_ENDIAN,
|
ByteOrder.BIG_ENDIAN,
|
||||||
MesgType.DAILY_WEATHER_FORECAST,
|
MesgType.DAILY_WEATHER_FORECAST,
|
||||||
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"),
|
||||||
new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"),
|
new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"),
|
||||||
new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"),
|
new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"),
|
||||||
new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"),
|
new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"),
|
||||||
new FieldDefinition(2, 1, BaseType.ENUM, "condition"),
|
new FieldDefinition(2, 1, BaseType.ENUM, "condition"),
|
||||||
|
@ -29,7 +29,7 @@ public class RecordData {
|
|||||||
|
|
||||||
for (FieldDefinition fieldDef :
|
for (FieldDefinition fieldDef :
|
||||||
recordDefinition.getFieldDefinitions()) {
|
recordDefinition.getFieldDefinitions()) {
|
||||||
fieldDataList.add(new FieldData(fieldDef.getBaseType(), totalSize, fieldDef.getSize(), fieldDef.getName(), fieldDef.getLocalNumber(), fieldDef.getScale(), fieldDef.getOffset()));
|
fieldDataList.add(new FieldData(fieldDef, totalSize));
|
||||||
totalSize += fieldDef.getSize();
|
totalSize += fieldDef.getSize();
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -90,10 +90,10 @@ public class RecordData {
|
|||||||
StringBuilder oBuilder = new StringBuilder();
|
StringBuilder oBuilder = new StringBuilder();
|
||||||
for (FieldData fieldData :
|
for (FieldData fieldData :
|
||||||
fieldDataList) {
|
fieldDataList) {
|
||||||
if (fieldData.getName() != null) {
|
if (fieldData.getName() != null && !fieldData.getName().equals("")) {
|
||||||
oBuilder.append(fieldData.getName());
|
oBuilder.append(fieldData.getName());
|
||||||
} else {
|
} else {
|
||||||
oBuilder.append(fieldData.getNumber());
|
oBuilder.append("unknown_" + fieldData.getNumber());
|
||||||
}
|
}
|
||||||
oBuilder.append(": ");
|
oBuilder.append(": ");
|
||||||
oBuilder.append(fieldData.decode());
|
oBuilder.append(fieldData.decode());
|
||||||
@ -104,45 +104,46 @@ public class RecordData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class FieldData {
|
public FieldData getFieldByNumber(int number) {
|
||||||
private final BaseType baseType;
|
for (FieldData fieldData :
|
||||||
|
fieldDataList) {
|
||||||
|
if (number == fieldData.getNumber()) {
|
||||||
|
return fieldData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FieldData {
|
||||||
|
private FieldDefinition fieldDefinition;
|
||||||
private final int position;
|
private final int position;
|
||||||
private final int size;
|
private final int size;
|
||||||
private final String name;
|
|
||||||
private final int scale;
|
public FieldData(FieldDefinition fieldDefinition, int position) {
|
||||||
private final int offset;
|
this.fieldDefinition = fieldDefinition;
|
||||||
private final int number;
|
|
||||||
public FieldData(BaseType baseType, int position, int size, String name, int number, int scale, int offset) {
|
|
||||||
this.baseType = baseType;
|
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.size = size;
|
this.size = fieldDefinition.getSize();
|
||||||
this.name = name;
|
|
||||||
this.number = number;
|
|
||||||
this.scale = scale;
|
|
||||||
this.offset = offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseType getBaseType() {
|
|
||||||
return baseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return fieldDefinition.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNumber() {
|
public int getNumber() {
|
||||||
return number;
|
return fieldDefinition.getLocalNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void invalidate() {
|
public void invalidate() {
|
||||||
goToPosition();
|
goToPosition();
|
||||||
if (STRING.equals(getBaseType())) {
|
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
valueHolder.put((byte) 0);
|
valueHolder.put((byte) 0);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
baseType.invalidate(valueHolder);
|
fieldDefinition.invalidate(valueHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void goToPosition() {
|
private void goToPosition() {
|
||||||
@ -159,18 +160,18 @@ public class RecordData {
|
|||||||
throw new IllegalArgumentException("Array of values not supported yet"); //TODO: handle arrays
|
throw new IllegalArgumentException("Array of values not supported yet"); //TODO: handle arrays
|
||||||
Object o = objects[0];
|
Object o = objects[0];
|
||||||
goToPosition();
|
goToPosition();
|
||||||
if (STRING.equals(getBaseType())) {
|
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||||
final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8);
|
final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8);
|
||||||
valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length)));
|
valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length)));
|
||||||
valueHolder.put((byte) 0);
|
valueHolder.put((byte) 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getBaseType().encode(valueHolder, o, scale, offset);
|
fieldDefinition.encode(valueHolder, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object decode() {
|
public Object decode() {
|
||||||
goToPosition();
|
goToPosition();
|
||||||
if (STRING.equals(getBaseType())) {
|
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||||
final byte[] bytes = new byte[size];
|
final byte[] bytes = new byte[size];
|
||||||
valueHolder.get(bytes);
|
valueHolder.get(bytes);
|
||||||
final int zero = ArrayUtils.indexOf((byte) 0, bytes);
|
final int zero = ArrayUtils.indexOf((byte) 0, bytes);
|
||||||
@ -180,7 +181,8 @@ public class RecordData {
|
|||||||
return new String(bytes, 0, zero, StandardCharsets.UTF_8);
|
return new String(bytes, 0, zero, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
//TODO: handle arrays
|
//TODO: handle arrays
|
||||||
return getBaseType().decode(valueHolder, scale, offset);
|
return fieldDefinition.decode(valueHolder);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||||
|
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils.GARMIN_TIME_EPOCH;
|
||||||
|
|
||||||
|
public class FieldDefinitionTimestamp extends FieldDefinition {
|
||||||
|
public FieldDefinitionTimestamp(int localNumber, int size, BaseType baseType, String name) {
|
||||||
|
super(localNumber, size, baseType, name, 1, GARMIN_TIME_EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public Object decode(ByteBuffer byteBuffer) {
|
||||||
|
// return new Timestamp((long) baseType.decode(byteBuffer, scale, offset) * 1000L);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void encode(ByteBuffer byteBuffer, Object o) {
|
||||||
|
// if(o instanceof Timestamp) {
|
||||||
|
// baseType.encode(byteBuffer, (int) (((Timestamp) o).getTime() / 1000L), scale, offset);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// baseType.encode(byteBuffer, o, scale, offset);
|
||||||
|
// }
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user