Skip to content

Commit fd67d77

Browse files
tsickingsfeilmeier
andauthored
SunSpec: improvements (OpenEMS#2337)
- There are SunSpec Channels which have a constant scale factor, e.g. S305_LAT has a scale factor of -7. The current implementation of the AbstractOpenemsSunSpecComponent cannot handle these, but only those whose scale factor is read via modbus. This PR handles this issue. - Moreover, a DummySunSpecComponent with all default models is added for JUnit testing, as well as a small JUnit test checking that the modbus tasks are not too long. - DummySunSpecComponent: initialize SunSpec modals in ASC order - Refactor addReadTasks to preprocessModbusElements to be able to reuse it for WriteTasks - Create one FC16WriteRegistersTask for all writeable registers of one block This solves some perfomance problems we had with multiple WriteTasks being executed on every Cycle (e.g. write ActivePower and ReactivePower channels) - Extract generateElementToChannelConverter logic was split to two places before; also now I am always applying the 'isDefined' check - even for Points with Scale-Factor - ScaleFactor: get next value to win one cycle (NOTE: this is still too slow with one cycle, but that will be tackled in another PR) --------- Co-authored-by: Stefan Feilmeier <[email protected]>
1 parent 066d06c commit fd67d77

File tree

7 files changed

+235
-86
lines changed

7 files changed

+235
-86
lines changed

cnf/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<!-- On update: also update gradle.properties file -->
1212
<groupId>biz.aQute.bnd.workspace</groupId>
1313
<artifactId>biz.aQute.bnd.workspace.gradle.plugin</artifactId>
14-
<version>6.4.0</version>
14+
<version>7.0.0</version>
1515
</dependency>
1616
<!-- com -->
1717
<dependency>

io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ElementToChannelScaleFactorConverter.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
*/
1919
public class ElementToChannelScaleFactorConverter extends ElementToChannelConverter {
2020

21+
private static int getValueOrError(OpenemsComponent component, ChannelId channelId)
22+
throws InvalidValueException, IllegalArgumentException {
23+
var channel = (IntegerReadChannel) component.channel(channelId);
24+
var value = channel.getNextValue().orElse(null);
25+
if (value != null) {
26+
return value;
27+
}
28+
return channel.value().getOrError();
29+
}
30+
2131
public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecPoint point,
2232
ChannelId scaleFactorChannel) {
2333
super(//
@@ -27,8 +37,7 @@ public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecP
2737
return null;
2838
}
2939
try {
30-
return apply(value,
31-
((IntegerReadChannel) component.channel(scaleFactorChannel)).value().getOrError() * -1);
40+
return apply(value, getValueOrError(component, scaleFactorChannel) * -1);
3241
} catch (InvalidValueException | IllegalArgumentException e) {
3342
return null;
3443
}
@@ -37,8 +46,7 @@ public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecP
3746
// channel -> element
3847
value -> {
3948
try {
40-
return apply(value,
41-
((IntegerReadChannel) component.channel(scaleFactorChannel)).value().getOrError());
49+
return apply(value, getValueOrError(component, scaleFactorChannel));
4250
} catch (InvalidValueException | IllegalArgumentException e) {
4351
return null;
4452
}

io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponent.java

+115-80
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package io.openems.edge.bridge.modbus.sunspec;
22

3-
import java.util.ArrayDeque;
43
import java.util.ArrayList;
5-
import java.util.Deque;
64
import java.util.List;
75
import java.util.Map;
86
import java.util.Map.Entry;
@@ -17,18 +15,20 @@
1715
import org.slf4j.Logger;
1816
import org.slf4j.LoggerFactory;
1917

18+
import com.google.common.collect.Lists;
19+
2020
import io.openems.common.exceptions.OpenemsException;
2121
import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
2222
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
2323
import io.openems.edge.bridge.modbus.api.ElementToChannelScaleFactorConverter;
2424
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
2525
import io.openems.edge.bridge.modbus.api.ModbusUtils;
26-
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
2726
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
2827
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
2928
import io.openems.edge.bridge.modbus.api.element.ModbusRegisterElement;
3029
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
3130
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
31+
import io.openems.edge.bridge.modbus.api.task.AbstractTask;
3232
import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
3333
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
3434
import io.openems.edge.bridge.modbus.api.task.Task;
@@ -291,102 +291,137 @@ public boolean isSunSpecInitializationCompleted() {
291291
protected void addBlock(int startAddress, SunSpecModel model, Priority priority) throws OpenemsException {
292292
this.logInfo(this.log, "Adding SunSpec-Model [" + model.getBlockId() + ":" + model.label() + "] starting at ["
293293
+ startAddress + "]");
294-
Deque<ModbusElement> elements = new ArrayDeque<>();
294+
var readElements = new ArrayList<ModbusElement>();
295+
var writeElements = new ArrayList<ModbusElement>();
295296
startAddress += 2;
296297
for (var i = 0; i < model.points().length; i++) {
297298
var point = model.points()[i];
298-
var element = point.get().generateModbusElement(startAddress);
299-
startAddress += element.length;
300-
elements.add(element);
299+
final var element = point.get().generateModbusElement(startAddress);
301300

301+
// Handle AccessMode
302+
switch (point.get().accessMode) {
303+
case READ_ONLY -> {
304+
readElements.add(element);
305+
}
306+
case READ_WRITE -> {
307+
readElements.add(element);
308+
writeElements.add(element);
309+
}
310+
case WRITE_ONLY -> {
311+
readElements.add(new DummyRegisterElement(element.startAddress, element.length));
312+
writeElements.add(element);
313+
}
314+
}
315+
316+
startAddress += element.length;
302317
var channelId = point.getChannelId();
303318
this.addChannel(channelId);
319+
this.m(channelId, element, this.generateElementToChannelConverter(model, point));
320+
}
304321

305-
if (point.get().scaleFactor.isPresent()) {
306-
// This Point needs a ScaleFactor
307-
// - find the ScaleFactor-Point
308-
var scaleFactorName = SunSpecCodeGenerator.toUpperUnderscore(point.get().scaleFactor.get());
309-
SunSpecPoint scaleFactorPoint = null;
310-
for (var sfPoint : model.points()) {
311-
if (sfPoint.name().equals(scaleFactorName)) {
312-
scaleFactorPoint = sfPoint;
313-
break;
314-
}
315-
}
316-
if (scaleFactorPoint == null) {
317-
// Unable to find ScaleFactor-Point
318-
this.logError(this.log,
319-
"Unable to find ScaleFactor [" + scaleFactorName + "] for Point [" + point.name() + "]");
320-
}
322+
// Create Tasks and add them to the ModbusProtocol
323+
for (var elements : preprocessModbusElements(readElements)) {
324+
this.modbusProtocol.addTask(//
325+
new FC3ReadRegistersTask(//
326+
elements.get(0).startAddress, priority, elements.toArray(ModbusElement[]::new)));
327+
}
328+
for (var elements : preprocessModbusElements(writeElements)) {
329+
this.modbusProtocol.addTask(//
330+
new FC16WriteRegistersTask(//
331+
elements.get(0).startAddress, elements.toArray(ModbusElement[]::new)));
332+
}
333+
}
321334

322-
// Add a scale-factor mapping between Element and Channel
323-
element = this.m(channelId, element,
324-
new ElementToChannelScaleFactorConverter(this, point, scaleFactorPoint.getChannelId()));
335+
/**
336+
* Converts a list of {@link ModbusElement}s to sublists, prepared for Modbus
337+
* {@link AbstractTask}s.
338+
*
339+
* <ul>
340+
* <li>Sublists are without holes (i.e. nextStartAddress = currentStartAddress +
341+
* Length + 1)
342+
* <li>Length of sublist <= MAXIMUM_TASK_LENGTH
343+
* </ul>
344+
*
345+
* @param elements the source elements
346+
* @return list of {@link ModbusElement} lists
347+
*/
348+
protected static List<List<ModbusElement>> preprocessModbusElements(List<ModbusElement> elements) {
349+
var result = Lists.<List<ModbusElement>>newArrayList(Lists.<ModbusElement>newArrayList());
350+
for (var element : elements) {
351+
// Get last sublist in result
352+
var l = result.get(result.size() - 1);
353+
// Get last element of sublist
354+
var e = l.isEmpty() ? null : l.get(l.size() - 1);
355+
if ((
356+
// Is first element of the sublist?
357+
e == null
358+
// Is element direct successor?
359+
|| e.startAddress + e.length == element.startAddress) //
360+
&& // Does element fit in task?
361+
l.stream().mapToInt(m -> m.length).sum() + element.length <= MAXIMUM_TASK_LENGTH //
362+
) {
363+
l.add(element); // Add to existing sublist
325364

326365
} else {
327-
// Add a direct mapping between Element and Channel
328-
element = this.m(channelId, element, new ElementToChannelConverter(
329-
// Element -> Channel
330-
value -> {
331-
if (!point.isDefined(value)) {
332-
// This value is set to be 'UNDEFINED' for the given type by SunSpec
333-
return null;
334-
}
335-
return value;
336-
},
337-
// Channel -> Element
338-
value -> value));
339-
340-
}
341-
342-
// Evaluate Access-Mode of the Channel
343-
switch (point.get().accessMode) {
344-
case READ_ONLY:
345-
// Read-Only -> replace element with dummy
346-
element = new DummyRegisterElement(element.startAddress,
347-
element.startAddress + point.get().type.length - 1);
348-
break;
349-
case READ_WRITE:
350-
case WRITE_ONLY:
351-
// Add a Write-Task
352-
// TODO create one FC16WriteRegistersTask for entire block
353-
final Task writeTask = new FC16WriteRegistersTask(element.startAddress, element);
354-
this.modbusProtocol.addTask(writeTask);
355-
break;
366+
result.add(Lists.<ModbusElement>newArrayList(element)); // Create new sublist
356367
}
357368
}
358369

359-
this.addReadTasks(elements, priority);
370+
// Avoid length check for sublist
371+
if (result.get(0).isEmpty()) {
372+
return List.of();
373+
}
374+
return result;
360375
}
361376

362377
/**
363-
* Splits the task if it is too long and adds the read tasks.
378+
* Generates a {@link ElementToChannelConverter} for a Point.
364379
*
365-
* @param elements the Deque of {@link ModbusElement}s for one block.
366-
* @param priority the reading priority
367-
* @throws OpenemsException on error
380+
* <ul>
381+
* <li>Check for UNDEFINED value as defined in SunSpec per Type specification
382+
* <li>If a Scale-Factor is defined, try to add it - either as other point of
383+
* model (e.g. "W_SF") or as static value converter
384+
* </ul>
385+
*
386+
* @param model the {@link SunSpecModel}
387+
* @param point the {@link SunSpecPoint}
388+
* @return an {@link ElementToChannelConverter}, never null
368389
*/
369-
private void addReadTasks(Deque<ModbusElement> elements, Priority priority) throws OpenemsException {
370-
var length = 0;
371-
var taskElements = new ArrayDeque<ModbusElement>();
372-
var element = elements.pollFirst();
373-
while (element != null) {
374-
if (length + element.length > MAXIMUM_TASK_LENGTH) {
375-
this.modbusProtocol.addTask(//
376-
new FC3ReadRegistersTask(//
377-
taskElements.peekFirst().startAddress, priority, //
378-
taskElements.toArray(new AbstractModbusElement[taskElements.size()])));
379-
length = 0;
380-
taskElements.clear();
381-
}
382-
taskElements.add(element);
383-
length += element.length;
384-
element = elements.pollFirst();
390+
protected ElementToChannelConverter generateElementToChannelConverter(SunSpecModel model, SunSpecPoint point) {
391+
// Create converter for 'defined' state
392+
final var valueIsDefinedConverter = new ElementToChannelConverter(//
393+
/* Element -> Channel */ value -> point.isDefined(value) ? value : null,
394+
/* Channel -> Element */ value -> value);
395+
396+
// Generate Scale-Factor converter (possibly null)
397+
ElementToChannelConverter scaleFactorConverter = null;
398+
if (point.get().scaleFactor.isPresent()) {
399+
final var scaleFactor = point.get().scaleFactor.get();
400+
final var scaleFactorName = SunSpecCodeGenerator.toUpperUnderscore(scaleFactor);
401+
scaleFactorConverter = Stream.of(model.points()) //
402+
.filter(p -> p.name().equals(scaleFactorName)) //
403+
.map(sfp -> new ElementToChannelScaleFactorConverter(this, point, sfp.getChannelId())) //
404+
// Found matching Scale-Factor Point in SunSpec Modal
405+
.findFirst()
406+
407+
// Else: try to parse constant Scale-Factor
408+
.orElseGet(() -> {
409+
try {
410+
return new ElementToChannelScaleFactorConverter(Integer.parseInt(scaleFactor));
411+
} catch (NumberFormatException e) {
412+
// Unable to parse Scale-Factor to static value
413+
this.logError(this.log, "Unable to parse Scale-Factor [" + scaleFactor + "] for Point ["
414+
+ point.name() + "]");
415+
return null;
416+
}
417+
}); //
418+
}
419+
420+
if (scaleFactorConverter != null) {
421+
return ElementToChannelConverter.chain(valueIsDefinedConverter, scaleFactorConverter);
422+
} else {
423+
return valueIsDefinedConverter;
385424
}
386-
this.modbusProtocol.addTask(//
387-
new FC3ReadRegistersTask(//
388-
taskElements.peekFirst().startAddress, priority, //
389-
taskElements.toArray(new AbstractModbusElement[taskElements.size()])));
390425
}
391426

392427
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.openems.edge.bridge.modbus.sunspec;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.util.ArrayList;
6+
7+
import org.junit.Test;
8+
9+
import io.openems.common.exceptions.OpenemsException;
10+
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
11+
import io.openems.edge.bridge.modbus.api.element.StringWordElement;
12+
13+
public class AbstractOpenemsSunSpecComponentTest {
14+
15+
@Test
16+
public void testPreprocessModbusElements() throws OpenemsException {
17+
var elements = new ArrayList<ModbusElement>();
18+
var startAddress = 0;
19+
for (var point : DefaultSunSpecModel.S_701.points()) {
20+
var element = point.get().generateModbusElement(startAddress);
21+
startAddress += element.length;
22+
elements.add(element);
23+
}
24+
25+
var sut = AbstractOpenemsSunSpecComponent.preprocessModbusElements(elements);
26+
assertEquals(2, sut.size()); // two sublists
27+
assertEquals(69, sut.get(0).size()); // first task
28+
assertEquals(1, sut.get(1).size()); // second task
29+
assertEquals(StringWordElement.class, sut.get(1).get(0).getClass()); // second task
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.openems.edge.bridge.modbus.sunspec;
2+
3+
import java.util.Map;
4+
import java.util.TreeMap;
5+
import java.util.stream.Collectors;
6+
import java.util.stream.Stream;
7+
8+
import io.openems.common.exceptions.OpenemsException;
9+
import io.openems.edge.bridge.modbus.api.ModbusComponent;
10+
import io.openems.edge.bridge.modbus.api.task.Task;
11+
import io.openems.edge.common.component.OpenemsComponent;
12+
import io.openems.edge.common.taskmanager.Priority;
13+
14+
public class DummySunSpecComponent extends AbstractOpenemsSunSpecComponent {
15+
16+
/**
17+
* All models are active with low priority.
18+
*/
19+
private static final Map<SunSpecModel, Priority> ACTIVE_MODELS = Stream.of(DefaultSunSpecModel.values())
20+
.collect(Collectors.toMap(model -> model, model -> Priority.LOW, (a, b) -> a, TreeMap::new));
21+
22+
public DummySunSpecComponent() throws OpenemsException {
23+
super(ACTIVE_MODELS, //
24+
OpenemsComponent.ChannelId.values(), //
25+
ModbusComponent.ChannelId.values()); //
26+
this.addBlocks();
27+
}
28+
29+
private void addBlocks() throws OpenemsException {
30+
var startAddress = 40000;
31+
for (var entry : ACTIVE_MODELS.keySet()) {
32+
this.addBlock(startAddress, entry, ACTIVE_MODELS.get(entry));
33+
}
34+
35+
}
36+
37+
@Override
38+
protected void onSunSpecInitializationCompleted() {
39+
}
40+
41+
/**
42+
* Gets the length of the longest modbus task.
43+
*
44+
* @return the maximum task length
45+
* @throws OpenemsException on error
46+
*/
47+
public int maximumTaskLenghth() throws OpenemsException {
48+
return this.getModbusProtocol() //
49+
.getTaskManager() //
50+
.getTasks() //
51+
.stream() //
52+
.mapToInt(Task::getLength) //
53+
.max().orElse(0);
54+
55+
}
56+
57+
}

0 commit comments

Comments
 (0)