Skip to content

Commit

Permalink
Modbus Bridge: optimize execution of Tasks (#1976)
Browse files Browse the repository at this point in the history
This Pull-Request represents a complete rewrite of the Modbus Bridge. For information is available at:
- https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.bridge.modbus/readme.adoc
- https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.bridge.modbus/doc/statemachine.md
- https://community.openems.io/t/asking-for-feedback-new-modbusworker-implementation/1747/2

**Breaking changes:**
- Some names of Interfaces and Classes have changed.
- It might be required in your individual implementation of a ModbusComponent, to explicitely import `com.ghgande.j2mod` in the bnd file

# Implementation details

OpenEMS Components that use Modbus communication, must implement the `ModbusComponent` interface and provide a `ModbusProtocol`. A protocol uses the notion of a `Task` to define an individual Modbus Read or Write request that can cover multiple Modbus Registers or Coils depending on the Modbus function code. It is possible to add or remove tasks to/from a protocol at runtime or to change the execution `Priority`. The Modbus Bridge (`Bridge Modbus/RTU Serial` or `Bridge Modbus/TCP`) collects all protocols and manages the execution of Tasks.

### Execution of Modbus Tasks

Execution of Modbus Tasks is managed by the `ModbusWorker`. It...

* executes Write-Tasks as early as possible (directly after the EXECUTE_WRITE event)
* executes Read-Tasks as late as possible to have values available exactly when they are needed (i.e. just before the BEFORE_PROCESS_IMAGE event). To achieve this, the ModbusWorker evaluates all execution times and 'learns' an ideal delay time, that is applied on every Cycle - the 'CycleDelay'
* handles defective ModbusComponents (i.e. ones where tasks have repeatedly failed) and delays reading from/writing to those components in order to avoid having defective components block the entire communication bus. Maximum delay is 5 minutes for read from defective components. ModbusComponents can trigger a retry from a defective Component by calling the `retryModbusCommunication()` method.

### Priority

Read-Tasks can have two different priorities, that are defined in the ModbusProtocol definition:

* `HIGH`: the task is executed once every Cycle
* `LOW`: only one task of all defined LOW priority tasks of all components registered on the same bridge is executed per Cycle
Write-Tasks always have `HIGH` priority, i.e. a set-point is always executed as-soon-as-possible - as long as the Component is not marked as defective

### Channels

Each Modbus Bridge provides Channels for more detailed information:

* `CycleTimeIsTooShort`: the configured global Cycle-Time is too short to execute all planned tasks in one Cycle
* `CycleDelay`: see 'CycleDelay' in the 'ModbusWorker' description above

### Logging

Often it is useful to print detailed logging information on the Console for debugging purposes. Logging can be enabled on Task level in the definition of the ModbusProtocol by adding `.debug()` or globally per Modbus Bridge via the `LogVerbosity` configuration parameter:

* `NONE`: Show no logs
* `DEBUG_LOG`: Shows basic logging information via the Controller.Debug.Log
* `READS_AND_WRITES`: Show logs for all read and write requests
* `READS_AND_WRITES_VERBOSE`: Show logs for all read and write requests, including actual hex or binary values of request and response
* `READS_AND_WRITES_DURATION`: Show logs for all read and write requests, including actual duration time per request
* `READS_AND_WRITES_DURATION_TRACE_EVENTS`: Show logs for all read and write requests, including actual duration time per request & trace the internal Event-based State-Machine

The log level via configuration parameter may be changed at any time during runtime without side-effects on the communication.
  • Loading branch information
sfeilmeier authored Aug 2, 2023
1 parent e27010f commit a37da1a
Show file tree
Hide file tree
Showing 126 changed files with 3,150 additions and 1,551 deletions.
4 changes: 2 additions & 2 deletions io.openems.edge.battery.bmw/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.bridge.modbus,\
io.openems.edge.common

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
4 changes: 2 additions & 2 deletions io.openems.edge.battery.bydcommercial/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.bridge.modbus,\
io.openems.edge.common,\
io.openems.edge.ess.api

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
4 changes: 2 additions & 2 deletions io.openems.edge.battery.fenecon.commercial/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.bridge.modbus,\
Expand All @@ -13,5 +14,4 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.io.api,\

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
4 changes: 2 additions & 2 deletions io.openems.edge.battery.fenecon.home/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.bridge.modbus,\
Expand All @@ -14,5 +15,4 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.io.api,\

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.openems.common.types.OpenemsType;
import io.openems.edge.battery.api.Battery;
import io.openems.edge.battery.fenecon.home.statemachine.StateMachine.State;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.common.channel.Channel;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.channel.IntegerDoc;
Expand All @@ -14,7 +15,7 @@
import io.openems.edge.common.startstop.StartStop;
import io.openems.edge.common.startstop.StartStoppable;

public interface BatteryFeneconHome extends Battery, OpenemsComponent, StartStoppable {
public interface BatteryFeneconHome extends Battery, ModbusComponent, OpenemsComponent, StartStoppable {

/**
* Gets the Channel for {@link ChannelId#BMS_CONTROL}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
Expand Down Expand Up @@ -738,8 +738,8 @@ private synchronized void initializeTowerModulesChannels(int numberOfTowers, int
* for Cell-Voltages.Channel-IDs are like "TOWER_0_OFFSET_2_TEMPERATURE_003".
* Channel-IDs are like "TOWER_0_OFFSET_2_VOLTAGE_003".
*/
var ameVolt = new AbstractModbusElement<?>[SENSORS_PER_MODULE];
var ameTemp = new AbstractModbusElement<?>[SENSORS_PER_MODULE];
var ameVolt = new ModbusElement<?>[SENSORS_PER_MODULE];
var ameTemp = new ModbusElement<?>[SENSORS_PER_MODULE];
for (var j = 0; j < SENSORS_PER_MODULE; j++) {
{
// Create Voltage Channel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ protected boolean isBatteryStarted() {
}
return !isNotStarted.get();
}

protected void retryModbusCommunication() {
this.getParent().retryModbusCommunication();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public State runAndGetNextState(Context context) throws OpenemsNamedException {
}

case FINISHED:
// Finished. Battery should have started up.
context.retryModbusCommunication();
return State.RUNNING;
}

Expand Down
4 changes: 2 additions & 2 deletions io.openems.edge.battery.soltaro/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.bridge.modbus,\
Expand All @@ -13,5 +14,4 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.ess.api

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
Original file line number Diff line number Diff line change
Expand Up @@ -682,17 +682,17 @@ private int getAddressContactorControl(int addressOffsetRack) {
return addressOffsetRack + OFFSET_CONTACTOR_CONTROL;
}

protected final AbstractModbusElement<?> map(io.openems.edge.common.channel.ChannelId channelId,
AbstractModbusElement<?> element) {
protected final <T extends AbstractModbusElement<?, ?>> T map(io.openems.edge.common.channel.ChannelId channelId,
T element) {
return this.m(channelId, element);
}

protected final AbstractModbusElement<?> map(io.openems.edge.common.channel.ChannelId channelId,
AbstractModbusElement<?> element, ElementToChannelConverter converter) {
protected final <T extends AbstractModbusElement<?, ?>> T map(io.openems.edge.common.channel.ChannelId channelId,
T element, ElementToChannelConverter converter) {
return this.m(channelId, element, converter);
}

protected final AbstractModbusElement<?> map(BitsWordElement bitsWordElement) {
protected final BitsWordElement map(BitsWordElement bitsWordElement) {
return super.m(bitsWordElement);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

Expand All @@ -16,8 +15,8 @@
import io.openems.common.types.OpenemsType;
import io.openems.edge.battery.soltaro.common.enums.ChargeIndication;
import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
Expand Down Expand Up @@ -253,44 +252,46 @@ protected Collection<Task> getTasks() {

// Cell voltages
for (var i = 0; i < this.numberOfSlaves; i++) {
List<AbstractModbusElement<?>> elements = new ArrayList<>();
var elements = new ArrayList<ModbusElement<?>>();
for (var j = i * VOLTAGE_SENSORS_PER_MODULE; j < (i + 1) * VOLTAGE_SENSORS_PER_MODULE; j++) {
var key = this.getSingleCellPrefix(j) + "_" + VOLTAGE;
var uwe = this.getUnsignedWordElement(VOLTAGE_ADDRESS_OFFSET + j);
AbstractModbusElement<?> ame = this.parent.map(this.channelIds.get(key), uwe);
var ame = this.parent.map(this.channelIds.get(key), uwe);
elements.add(ame);
}

// not more than 100 elements per task, because it can cause problems..
var taskCount = elements.size() / maxElementsPerTask + 1;

for (var x = 0; x < taskCount; x++) {
var subElements = elements.subList(x * maxElementsPerTask,
Math.min((x + 1) * maxElementsPerTask, elements.size()));
var taskElements = subElements.toArray(new AbstractModbusElement<?>[0]);
var taskElements = elements
.subList(x * maxElementsPerTask, Math.min((x + 1) * maxElementsPerTask, elements.size())) //
.stream() //
.toArray(ModbusElement[]::new);
tasks.add(new FC3ReadRegistersTask(taskElements[0].getStartAddress(), Priority.LOW, taskElements));
}

}

// Cell temperatures
for (var i = 0; i < this.numberOfSlaves; i++) {
List<AbstractModbusElement<?>> elements = new ArrayList<>();
var elements = new ArrayList<ModbusElement<?>>();
for (var j = i * TEMPERATURE_SENSORS_PER_MODULE; j < (i + 1) * TEMPERATURE_SENSORS_PER_MODULE; j++) {
var key = this.getSingleCellPrefix(j) + "_" + TEMPERATURE;

var swe = this.getSignedWordElement(TEMPERATURE_ADDRESS_OFFSET + j);
AbstractModbusElement<?> ame = this.parent.map(this.channelIds.get(key), swe);
var ame = this.parent.map(this.channelIds.get(key), swe);
elements.add(ame);
}

// not more than 100 elements per task, because it can cause problems..
var taskCount = elements.size() / maxElementsPerTask + 1;

for (var x = 0; x < taskCount; x++) {
var subElements = elements.subList(x * maxElementsPerTask,
Math.min((x + 1) * maxElementsPerTask, elements.size()));
var taskElements = subElements.toArray(new AbstractModbusElement<?>[0]);
var taskElements = elements
.subList(x * maxElementsPerTask, Math.min((x + 1) * maxElementsPerTask, elements.size())) //
.stream() //
.toArray(ModbusElement[]::new);
tasks.add(new FC3ReadRegistersTask(taskElements[0].getStartAddress(), Priority.LOW, taskElements));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.ModbusUtils;
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
Expand Down Expand Up @@ -386,7 +386,7 @@ private void updateRackChannels(Integer numberOfModules, TreeSet<Rack> racks) th
} //
Consumer<CellChannelFactory.Type> addCellChannels = type -> {
for (var i = 0; i < numberOfModules; i++) {
var elements = new AbstractModbusElement<?>[type.getSensorsPerModule()];
var elements = new ModbusElement<?>[type.getSensorsPerModule()];
for (var j = 0; j < type.getSensorsPerModule(); j++) {
var sensorIndex = i * type.getSensorsPerModule() + j;
var channelId = CellChannelFactory.create(r, type, sensorIndex);
Expand All @@ -411,7 +411,7 @@ private void updateRackChannels(Integer numberOfModules, TreeSet<Rack> racks) th

// WARN_LEVEL_Pre Alarm (Pre Alarm configuration registers RW)
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(this.createChannelId(r, RackChannel.PRE_ALARM_CELL_OVER_VOLTAGE_ALARM),
new UnsignedWordElement(r.offset + 0x080)), //
m(this.createChannelId(r, RackChannel.PRE_ALARM_CELL_OVER_VOLTAGE_RECOVER),
Expand Down Expand Up @@ -486,7 +486,7 @@ private void updateRackChannels(Integer numberOfModules, TreeSet<Rack> racks) th

// WARN_LEVEL1 (Level1 warning registers RW)
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(this.createChannelId(r, RackChannel.LEVEL1_CELL_OVER_VOLTAGE_PROTECTION),
new UnsignedWordElement(r.offset + 0x040)), //
m(this.createChannelId(r, RackChannel.LEVEL1_CELL_OVER_VOLTAGE_RECOVER),
Expand Down Expand Up @@ -561,7 +561,7 @@ private void updateRackChannels(Integer numberOfModules, TreeSet<Rack> racks) th

// WARN_LEVEL2 (Level2 Protection registers RW)
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(this.createChannelId(r, RackChannel.LEVEL2_CELL_OVER_VOLTAGE_PROTECTION),
new UnsignedWordElement(r.offset + 0x400)), //
m(this.createChannelId(r, RackChannel.LEVEL2_CELL_OVER_VOLTAGE_RECOVER),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.ModbusUtils;
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
Expand Down Expand Up @@ -966,8 +966,8 @@ private void calculateCapacity(Integer numberOfModules) {
private void createDynamicChannels(int numberOfModules) {
try {
for (var i = 0; i < numberOfModules; i++) {
var ameVolt = new AbstractModbusElement<?>[SENSORS_PER_MODULE];
var ameTemp = new AbstractModbusElement<?>[SENSORS_PER_MODULE];
var ameVolt = new ModbusElement<?>[SENSORS_PER_MODULE];
var ameTemp = new ModbusElement<?>[SENSORS_PER_MODULE];
for (var j = 0; j < SENSORS_PER_MODULE; j++) {
var sensor = i * SENSORS_PER_MODULE + j;
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.ModbusUtils;
import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.ModbusElement;
import io.openems.edge.bridge.modbus.api.element.SignedWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
Expand Down Expand Up @@ -492,7 +492,7 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException {
.bit(12, BatterySoltaroSingleRackVersionC.ChannelId.SLAVE_BMS_INIT)//
))); //
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(BatterySoltaroSingleRackVersionC.ChannelId.PRE_ALARM_CELL_OVER_VOLTAGE_ALARM,
new UnsignedWordElement(0x2080)), //
m(BatterySoltaroSingleRackVersionC.ChannelId.PRE_ALARM_CELL_OVER_VOLTAGE_RECOVER,
Expand Down Expand Up @@ -565,7 +565,7 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException {

// WARN_LEVEL1 (Level1 warning registers RW)
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(BatterySoltaroSingleRackVersionC.ChannelId.LEVEL1_CELL_OVER_VOLTAGE_PROTECTION,
new UnsignedWordElement(0x2040)), //
m(BatterySoltaroSingleRackVersionC.ChannelId.LEVEL1_CELL_OVER_VOLTAGE_RECOVER,
Expand Down Expand Up @@ -638,7 +638,7 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException {

// WARN_LEVEL2 (Level2 Protection registers RW)
{
AbstractModbusElement<?>[] elements = {
ModbusElement<?>[] elements = {
m(BatterySoltaroSingleRackVersionC.ChannelId.LEVEL2_CELL_OVER_VOLTAGE_PROTECTION,
new UnsignedWordElement(0x2400)), //
m(BatterySoltaroSingleRackVersionC.ChannelId.LEVEL2_CELL_OVER_VOLTAGE_RECOVER,
Expand Down Expand Up @@ -725,7 +725,7 @@ void createCellVoltageAndTemperatureChannels(int numberOfModules) {
*/
Consumer<CellChannelFactory.Type> addCellChannels = type -> {
for (var i = 0; i < numberOfModules; i++) {
var elements = new AbstractModbusElement<?>[type.getSensorsPerModule()];
var elements = new ModbusElement<?>[type.getSensorsPerModule()];
for (var j = 0; j < type.getSensorsPerModule(); j++) {
var sensorIndex = i * type.getSensorsPerModule() + j;
var channelId = CellChannelFactory.create(type, sensorIndex);
Expand Down
4 changes: 2 additions & 2 deletions io.openems.edge.batteryinverter.refu88k/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.batteryinverter.api,\
Expand All @@ -14,5 +15,4 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.timedata.api,\

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
4 changes: 2 additions & 2 deletions io.openems.edge.batteryinverter.sinexcel/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.battery.api,\
io.openems.edge.batteryinverter.api,\
Expand All @@ -14,5 +15,4 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.timedata.api,\

-testpath: \
${testpath},\
com.ghgande.j2mod
${testpath}
Loading

0 comments on commit a37da1a

Please sign in to comment.