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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.Collection;
|
||||
|
||||
@RestController
|
||||
public class TelemetryController {
|
||||
|
||||
private final SensorTelemetryReader sensorTelemetryReader;
|
||||
private final TelemetryCache telemetryCache;
|
||||
|
||||
public TelemetryController(SensorTelemetryReader sensorTelemetryReader) {
|
||||
public TelemetryController(
|
||||
SensorTelemetryReader sensorTelemetryReader,
|
||||
TelemetryCache telemetryCache
|
||||
) {
|
||||
this.sensorTelemetryReader = sensorTelemetryReader;
|
||||
this.telemetryCache = telemetryCache;
|
||||
}
|
||||
|
||||
@GetMapping("/api/telemetry/sensors/{sensorId}")
|
||||
public TelemetrySnapshot readSensor(@PathVariable Integer sensorId) {
|
||||
return sensorTelemetryReader.readSensor(sensorId);
|
||||
}
|
||||
|
||||
@GetMapping("/api/telemetry/latest")
|
||||
public Collection<TelemetrySnapshot> getLatest() {
|
||||
return telemetryCache.getAll();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user