|
1 | 1 | package io.openems.edge.bridge.modbus.sunspec;
|
2 | 2 |
|
3 |
| -import java.util.ArrayDeque; |
4 | 3 | import java.util.ArrayList;
|
5 |
| -import java.util.Deque; |
6 | 4 | import java.util.List;
|
7 | 5 | import java.util.Map;
|
8 | 6 | import java.util.Map.Entry;
|
|
17 | 15 | import org.slf4j.Logger;
|
18 | 16 | import org.slf4j.LoggerFactory;
|
19 | 17 |
|
| 18 | +import com.google.common.collect.Lists; |
| 19 | + |
20 | 20 | import io.openems.common.exceptions.OpenemsException;
|
21 | 21 | import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
|
22 | 22 | import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
|
23 | 23 | import io.openems.edge.bridge.modbus.api.ElementToChannelScaleFactorConverter;
|
24 | 24 | import io.openems.edge.bridge.modbus.api.ModbusProtocol;
|
25 | 25 | import io.openems.edge.bridge.modbus.api.ModbusUtils;
|
26 |
| -import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement; |
27 | 26 | import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
|
28 | 27 | import io.openems.edge.bridge.modbus.api.element.ModbusElement;
|
29 | 28 | import io.openems.edge.bridge.modbus.api.element.ModbusRegisterElement;
|
30 | 29 | import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
|
31 | 30 | import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
|
| 31 | +import io.openems.edge.bridge.modbus.api.task.AbstractTask; |
32 | 32 | import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
|
33 | 33 | import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
|
34 | 34 | import io.openems.edge.bridge.modbus.api.task.Task;
|
@@ -291,102 +291,137 @@ public boolean isSunSpecInitializationCompleted() {
|
291 | 291 | protected void addBlock(int startAddress, SunSpecModel model, Priority priority) throws OpenemsException {
|
292 | 292 | this.logInfo(this.log, "Adding SunSpec-Model [" + model.getBlockId() + ":" + model.label() + "] starting at ["
|
293 | 293 | + startAddress + "]");
|
294 |
| - Deque<ModbusElement> elements = new ArrayDeque<>(); |
| 294 | + var readElements = new ArrayList<ModbusElement>(); |
| 295 | + var writeElements = new ArrayList<ModbusElement>(); |
295 | 296 | startAddress += 2;
|
296 | 297 | for (var i = 0; i < model.points().length; i++) {
|
297 | 298 | 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); |
301 | 300 |
|
| 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; |
302 | 317 | var channelId = point.getChannelId();
|
303 | 318 | this.addChannel(channelId);
|
| 319 | + this.m(channelId, element, this.generateElementToChannelConverter(model, point)); |
| 320 | + } |
304 | 321 |
|
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 | + } |
321 | 334 |
|
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 |
325 | 364 |
|
326 | 365 | } 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 |
356 | 367 | }
|
357 | 368 | }
|
358 | 369 |
|
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; |
360 | 375 | }
|
361 | 376 |
|
362 | 377 | /**
|
363 |
| - * Splits the task if it is too long and adds the read tasks. |
| 378 | + * Generates a {@link ElementToChannelConverter} for a Point. |
364 | 379 | *
|
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 |
368 | 389 | */
|
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; |
385 | 424 | }
|
386 |
| - this.modbusProtocol.addTask(// |
387 |
| - new FC3ReadRegistersTask(// |
388 |
| - taskElements.peekFirst().startAddress, priority, // |
389 |
| - taskElements.toArray(new AbstractModbusElement[taskElements.size()]))); |
390 | 425 | }
|
391 | 426 |
|
392 | 427 | /**
|
|
0 commit comments