Optimize acquisition polling with register blocks
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record AcquisitionBlock(
|
||||||
|
Integer startingAddress,
|
||||||
|
Integer quantity,
|
||||||
|
List<Integer> sensorIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinition;
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AcquisitionBlockPlanner {
|
||||||
|
|
||||||
|
private static final int BLOCK_SIZE = 40;
|
||||||
|
|
||||||
|
private final AcquisitionPlanBuilder acquisitionPlanBuilder;
|
||||||
|
private final SensorDefinitionRepository sensorDefinitionRepository;
|
||||||
|
|
||||||
|
public AcquisitionBlockPlanner(
|
||||||
|
AcquisitionPlanBuilder acquisitionPlanBuilder,
|
||||||
|
SensorDefinitionRepository sensorDefinitionRepository
|
||||||
|
) {
|
||||||
|
this.acquisitionPlanBuilder = acquisitionPlanBuilder;
|
||||||
|
this.sensorDefinitionRepository = sensorDefinitionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AcquisitionBlock> buildBlocks() {
|
||||||
|
AcquisitionPlan plan = acquisitionPlanBuilder.buildPlan();
|
||||||
|
|
||||||
|
List<SensorDefinition> sensors = sensorDefinitionRepository.findAllById(plan.sensorIds())
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(SensorDefinition::getModbusAddress))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<AcquisitionBlock> blocks = new ArrayList<>();
|
||||||
|
|
||||||
|
List<Integer> currentSensorIds = new ArrayList<>();
|
||||||
|
|
||||||
|
Integer currentBlockStart = null;
|
||||||
|
Integer currentBlockEnd = null;
|
||||||
|
|
||||||
|
for (SensorDefinition sensor : sensors) {
|
||||||
|
Integer address = sensor.getModbusAddress();
|
||||||
|
|
||||||
|
if (address == null || address < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBlockStart == null) {
|
||||||
|
currentBlockStart = address;
|
||||||
|
currentBlockEnd = address;
|
||||||
|
currentSensorIds.add(sensor.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean fitsCurrentBlock =
|
||||||
|
address <= currentBlockStart + BLOCK_SIZE
|
||||||
|
&& (address - currentBlockStart) <= BLOCK_SIZE;
|
||||||
|
|
||||||
|
if (fitsCurrentBlock) {
|
||||||
|
currentBlockEnd = address;
|
||||||
|
currentSensorIds.add(sensor.getId());
|
||||||
|
} else {
|
||||||
|
blocks.add(new AcquisitionBlock(
|
||||||
|
currentBlockStart,
|
||||||
|
(currentBlockEnd - currentBlockStart) + 1,
|
||||||
|
List.copyOf(currentSensorIds)
|
||||||
|
));
|
||||||
|
|
||||||
|
currentSensorIds.clear();
|
||||||
|
|
||||||
|
currentBlockStart = address;
|
||||||
|
currentBlockEnd = address;
|
||||||
|
|
||||||
|
currentSensorIds.add(sensor.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBlockStart != null && !currentSensorIds.isEmpty()) {
|
||||||
|
blocks.add(new AcquisitionBlock(
|
||||||
|
currentBlockStart,
|
||||||
|
(currentBlockEnd - currentBlockStart) + 1,
|
||||||
|
List.copyOf(currentSensorIds)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class AcquisitionController {
|
||||||
|
|
||||||
|
private final AcquisitionPlanBuilder acquisitionPlanBuilder;
|
||||||
|
private final AcquisitionPollingService acquisitionPollingService;
|
||||||
|
private final BlockPollingService blockPollingService;
|
||||||
|
public AcquisitionController(
|
||||||
|
AcquisitionPlanBuilder acquisitionPlanBuilder,
|
||||||
|
AcquisitionPollingService acquisitionPollingService,
|
||||||
|
BlockPollingService blockPollingService
|
||||||
|
) {
|
||||||
|
this.acquisitionPlanBuilder = acquisitionPlanBuilder;
|
||||||
|
this.acquisitionPollingService = acquisitionPollingService;
|
||||||
|
this.blockPollingService = blockPollingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/acquisition/plan")
|
||||||
|
public AcquisitionPlan getPlan() {
|
||||||
|
return acquisitionPlanBuilder.buildPlan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/acquisition/poll-once")
|
||||||
|
public AcquisitionPollResult pollOnce() {
|
||||||
|
return acquisitionPollingService.pollOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/acquisition/poll-once-blocks")
|
||||||
|
public AcquisitionPollResult pollOnceByBlocks() {
|
||||||
|
return blockPollingService.pollOnceByBlocks();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record AcquisitionPlan(
|
||||||
|
Integer climateGreenhouseCount,
|
||||||
|
Integer irrigationControllerCount,
|
||||||
|
Boolean climateEnabled,
|
||||||
|
Boolean irrigationEnabled,
|
||||||
|
Boolean lightingEnabled,
|
||||||
|
Integer sensorCount,
|
||||||
|
List<Integer> sensorIds,
|
||||||
|
Instant timestamp
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinition;
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
|
||||||
|
import com.litoralregas.backend.sensor.SensorSourceType;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AcquisitionPlanBuilder {
|
||||||
|
|
||||||
|
private final ControllerCapabilitiesReader capabilitiesReader;
|
||||||
|
private final SensorDefinitionRepository sensorDefinitionRepository;
|
||||||
|
|
||||||
|
public AcquisitionPlanBuilder(
|
||||||
|
ControllerCapabilitiesReader capabilitiesReader,
|
||||||
|
SensorDefinitionRepository sensorDefinitionRepository
|
||||||
|
) {
|
||||||
|
this.capabilitiesReader = capabilitiesReader;
|
||||||
|
this.sensorDefinitionRepository = sensorDefinitionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcquisitionPlan buildPlan() {
|
||||||
|
ControllerCapabilities capabilities = capabilitiesReader.readCapabilities();
|
||||||
|
|
||||||
|
List<Integer> sensorIds = sensorDefinitionRepository.findByEnabledTrueOrderByNameAsc()
|
||||||
|
.stream()
|
||||||
|
.filter(sensor -> sensor.getSourceType() == SensorSourceType.MODBUS)
|
||||||
|
.filter(sensor -> shouldPollSensor(sensor, capabilities))
|
||||||
|
.map(SensorDefinition::getId)
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return new AcquisitionPlan(
|
||||||
|
capabilities.climateGreenhouseCount(),
|
||||||
|
capabilities.irrigationControllerCount(),
|
||||||
|
capabilities.climateEnabled(),
|
||||||
|
capabilities.irrigationEnabled(),
|
||||||
|
capabilities.lightingEnabled(),
|
||||||
|
sensorIds.size(),
|
||||||
|
sensorIds,
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldPollSensor(
|
||||||
|
SensorDefinition sensor,
|
||||||
|
ControllerCapabilities capabilities
|
||||||
|
) {
|
||||||
|
String category = sensor.getCategory();
|
||||||
|
|
||||||
|
if ("CLIMATE".equals(category)) {
|
||||||
|
return capabilities.climateEnabled()
|
||||||
|
&& belongsToEnabledClimateRange(sensor, capabilities.climateGreenhouseCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("IRRIGATION".equals(category)) {
|
||||||
|
return capabilities.irrigationEnabled()
|
||||||
|
&& belongsToEnabledIrrigationRange(sensor, capabilities.irrigationControllerCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("LIGHTING".equals(category)) {
|
||||||
|
return capabilities.lightingEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean belongsToEnabledClimateRange(
|
||||||
|
SensorDefinition sensor,
|
||||||
|
Integer greenhouseCount
|
||||||
|
) {
|
||||||
|
Integer address = sensor.getModbusAddress();
|
||||||
|
|
||||||
|
if (address == null || address < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address >= 10 && address <= 22) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address < 100 || address > 899) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int greenhouseNumber = ((address - 100) / 40) + 1;
|
||||||
|
|
||||||
|
return greenhouseNumber >= 1 && greenhouseNumber <= greenhouseCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean belongsToEnabledIrrigationRange(
|
||||||
|
SensorDefinition sensor,
|
||||||
|
Integer controllerCount
|
||||||
|
) {
|
||||||
|
Integer address = sensor.getModbusAddress();
|
||||||
|
|
||||||
|
if (address == null || address < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address < 1000 || address > 1599) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int controllerNumber = ((address - 1000) / 100) + 1;
|
||||||
|
|
||||||
|
return controllerNumber >= 1 && controllerNumber <= controllerCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record AcquisitionPollResult(
|
||||||
|
Integer plannedSensorCount,
|
||||||
|
Integer successfulReads,
|
||||||
|
Integer failedReads,
|
||||||
|
List<Integer> failedSensorIds,
|
||||||
|
Instant startedAt,
|
||||||
|
Instant finishedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AcquisitionPollingService {
|
||||||
|
|
||||||
|
private final AcquisitionPlanBuilder acquisitionPlanBuilder;
|
||||||
|
private final SensorTelemetryReader sensorTelemetryReader;
|
||||||
|
|
||||||
|
public AcquisitionPollingService(
|
||||||
|
AcquisitionPlanBuilder acquisitionPlanBuilder,
|
||||||
|
SensorTelemetryReader sensorTelemetryReader
|
||||||
|
) {
|
||||||
|
this.acquisitionPlanBuilder = acquisitionPlanBuilder;
|
||||||
|
this.sensorTelemetryReader = sensorTelemetryReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcquisitionPollResult pollOnce() {
|
||||||
|
Instant startedAt = Instant.now();
|
||||||
|
|
||||||
|
AcquisitionPlan plan = acquisitionPlanBuilder.buildPlan();
|
||||||
|
|
||||||
|
int successfulReads = 0;
|
||||||
|
|
||||||
|
List<Integer> failedSensorIds = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Integer sensorId : plan.sensorIds()) {
|
||||||
|
try {
|
||||||
|
sensorTelemetryReader.readSensor(sensorId);
|
||||||
|
successfulReads++;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
failedSensorIds.add(sensorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant finishedAt = Instant.now();
|
||||||
|
|
||||||
|
return new AcquisitionPollResult(
|
||||||
|
plan.sensorCount(),
|
||||||
|
successfulReads,
|
||||||
|
failedSensorIds.size(),
|
||||||
|
failedSensorIds,
|
||||||
|
startedAt,
|
||||||
|
finishedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.litoralregas.backend.acquisition;
|
||||||
|
|
||||||
|
import com.litoralregas.backend.modbus.LrModbusClient;
|
||||||
|
import com.litoralregas.backend.modbus.ModbusReadResult;
|
||||||
|
import com.litoralregas.backend.modbus.ModbusUnit;
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinition;
|
||||||
|
import com.litoralregas.backend.sensor.SensorDefinitionRepository;
|
||||||
|
import com.litoralregas.backend.sensor.SensorValueType;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BlockPollingService {
|
||||||
|
|
||||||
|
private final AcquisitionBlockPlanner blockPlanner;
|
||||||
|
private final SensorDefinitionRepository sensorDefinitionRepository;
|
||||||
|
private final LrModbusClient modbusClient;
|
||||||
|
private final TelemetryCache telemetryCache;
|
||||||
|
|
||||||
|
public BlockPollingService(
|
||||||
|
AcquisitionBlockPlanner blockPlanner,
|
||||||
|
SensorDefinitionRepository sensorDefinitionRepository,
|
||||||
|
LrModbusClient modbusClient,
|
||||||
|
TelemetryCache telemetryCache
|
||||||
|
) {
|
||||||
|
this.blockPlanner = blockPlanner;
|
||||||
|
this.sensorDefinitionRepository = sensorDefinitionRepository;
|
||||||
|
this.modbusClient = modbusClient;
|
||||||
|
this.telemetryCache = telemetryCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcquisitionPollResult pollOnceByBlocks() {
|
||||||
|
Instant startedAt = Instant.now();
|
||||||
|
|
||||||
|
List<AcquisitionBlock> blocks = blockPlanner.buildBlocks();
|
||||||
|
|
||||||
|
List<Integer> allSensorIds = blocks.stream()
|
||||||
|
.flatMap(block -> block.sensorIds().stream())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<Integer, SensorDefinition> sensorsById = sensorDefinitionRepository.findAllById(allSensorIds)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(SensorDefinition::getId, Function.identity()));
|
||||||
|
|
||||||
|
int successfulReads = 0;
|
||||||
|
List<Integer> failedSensorIds = new ArrayList<>();
|
||||||
|
|
||||||
|
for (AcquisitionBlock block : blocks) {
|
||||||
|
try {
|
||||||
|
ModbusReadResult result = modbusClient.readInputRegisters(
|
||||||
|
ModbusUnit.PC,
|
||||||
|
block.startingAddress(),
|
||||||
|
block.quantity()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (Integer sensorId : block.sensorIds()) {
|
||||||
|
SensorDefinition sensor = sensorsById.get(sensorId);
|
||||||
|
|
||||||
|
if (sensor == null) {
|
||||||
|
failedSensorIds.add(sensorId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Integer rawValue = rawValueForSensor(sensor, block, result);
|
||||||
|
Object value = convertValue(sensor, rawValue);
|
||||||
|
|
||||||
|
telemetryCache.put(new TelemetrySnapshot(
|
||||||
|
sensor.getId(),
|
||||||
|
sensor.getName(),
|
||||||
|
sensor.getModbusAddress(),
|
||||||
|
sensor.getBitOffset(),
|
||||||
|
rawValue,
|
||||||
|
value,
|
||||||
|
sensor.getUnit(),
|
||||||
|
Instant.now()
|
||||||
|
));
|
||||||
|
|
||||||
|
successfulReads++;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
failedSensorIds.add(sensorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
failedSensorIds.addAll(block.sensorIds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant finishedAt = Instant.now();
|
||||||
|
|
||||||
|
return new AcquisitionPollResult(
|
||||||
|
allSensorIds.size(),
|
||||||
|
successfulReads,
|
||||||
|
failedSensorIds.size(),
|
||||||
|
failedSensorIds,
|
||||||
|
startedAt,
|
||||||
|
finishedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer rawValueForSensor(
|
||||||
|
SensorDefinition sensor,
|
||||||
|
AcquisitionBlock block,
|
||||||
|
ModbusReadResult result
|
||||||
|
) {
|
||||||
|
int offset = sensor.getModbusAddress() - block.startingAddress();
|
||||||
|
|
||||||
|
if (offset < 0 || offset >= result.values().size()) {
|
||||||
|
throw new IllegalStateException("Sensor address outside acquisition block.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.values().get(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertValue(SensorDefinition sensor, Integer rawValue) {
|
||||||
|
if (sensor.getValueType() == SensorValueType.BOOLEAN) {
|
||||||
|
Integer bitOffset = sensor.getBitOffset();
|
||||||
|
|
||||||
|
if (bitOffset == null) {
|
||||||
|
throw new IllegalStateException("BOOLEAN sensor requires bitOffset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((rawValue >> bitOffset) & 1) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sensor.getValueType() == SensorValueType.DECIMAL) {
|
||||||
|
return rawValue / Math.pow(10, sensor.getDecimalPlaces());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,18 +3,29 @@ package com.litoralregas.backend.acquisition;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class TelemetryController {
|
public class TelemetryController {
|
||||||
|
|
||||||
private final SensorTelemetryReader sensorTelemetryReader;
|
private final SensorTelemetryReader sensorTelemetryReader;
|
||||||
|
private final TelemetryCache telemetryCache;
|
||||||
|
|
||||||
public TelemetryController(SensorTelemetryReader sensorTelemetryReader) {
|
public TelemetryController(
|
||||||
|
SensorTelemetryReader sensorTelemetryReader,
|
||||||
|
TelemetryCache telemetryCache
|
||||||
|
) {
|
||||||
this.sensorTelemetryReader = sensorTelemetryReader;
|
this.sensorTelemetryReader = sensorTelemetryReader;
|
||||||
|
this.telemetryCache = telemetryCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/api/telemetry/sensors/{sensorId}")
|
@GetMapping("/api/telemetry/sensors/{sensorId}")
|
||||||
public TelemetrySnapshot readSensor(@PathVariable Integer sensorId) {
|
public TelemetrySnapshot readSensor(@PathVariable Integer sensorId) {
|
||||||
return sensorTelemetryReader.readSensor(sensorId);
|
return sensorTelemetryReader.readSensor(sensorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/telemetry/latest")
|
||||||
|
public Collection<TelemetrySnapshot> getLatest() {
|
||||||
|
return telemetryCache.getAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user