├── softplc ├── src │ ├── test │ │ ├── resources │ │ │ ├── cl │ │ │ │ ├── file1.jar │ │ │ │ └── file2.jar │ │ │ ├── program │ │ │ │ ├── invalid.js │ │ │ │ ├── valid.js │ │ │ │ ├── main.js │ │ │ │ └── main_translated.js │ │ │ ├── file1.snapshot │ │ │ ├── composite.xml │ │ │ ├── config.xml │ │ │ └── softplc.dtd │ │ └── java │ │ │ └── de │ │ │ └── peteral │ │ │ └── softplc │ │ │ ├── reflection │ │ │ ├── TestInterface.java │ │ │ ├── TestClass1.java │ │ │ ├── TestClass2.java │ │ │ ├── TestAnnotation.java │ │ │ └── AnnotationProcessorTest.java │ │ │ ├── classloader │ │ │ └── FolderClassLoaderTest.java │ │ │ ├── symbol │ │ │ └── SymbolTableImplTest.java │ │ │ ├── main │ │ │ └── JavascriptTest.java │ │ │ ├── comm │ │ │ ├── common │ │ │ │ ├── ChangeRequestTest.java │ │ │ │ ├── ServerDataEventTest.java │ │ │ │ └── ClientChannelCacheTest.java │ │ │ ├── PutGetServerImplTest.java │ │ │ └── RequestWorkerTest.java │ │ │ ├── program │ │ │ ├── PrecompilerTest.java │ │ │ └── ProgramImplTest.java │ │ │ ├── datatype │ │ │ ├── BCDTest.java │ │ │ └── DataTypeFactoryTest.java │ │ │ ├── memorytables │ │ │ ├── MemoryTableWriteTaskTest.java │ │ │ └── MemoryTableUpdateTaskTest.java │ │ │ ├── view │ │ │ ├── ApplicationControllerTest.java │ │ │ └── JavaFXThreadingRule.java │ │ │ ├── memory │ │ │ ├── MemoryAreaImplTest.java │ │ │ └── MemoryIntegrationTest.java │ │ │ ├── address │ │ │ └── ParsedAddressTest.java │ │ │ ├── plc │ │ │ └── PlcImplTest.java │ │ │ ├── transformer │ │ │ └── PlcTransformerTest.java │ │ │ ├── factory │ │ │ └── PlcFactoryTest.java │ │ │ └── cpu │ │ │ └── CpuImplTest.java │ └── main │ │ ├── resources │ │ ├── script │ │ │ └── main.js │ │ └── images │ │ │ └── softplc_32.png │ │ └── java │ │ └── de │ │ └── peteral │ │ └── softplc │ │ ├── model │ │ ├── PutGetServerEvent.java │ │ ├── CpuStatus.java │ │ ├── PutGetServerObserver.java │ │ ├── SoftplcResponseFactory.java │ │ ├── CommunicationTask.java │ │ ├── MemoryAccessViolationException.java │ │ ├── SymbolTable.java │ │ ├── ProgramCycleObserver.java │ │ ├── ErrorLog.java │ │ ├── ResponseFactory.java │ │ ├── MemoryTable.java │ │ ├── Symbol.java │ │ ├── ScriptFile.java │ │ ├── PutGetServer.java │ │ ├── NetworkInterface.java │ │ ├── Program.java │ │ ├── Plc.java │ │ ├── Converter.java │ │ ├── ErrorLogEntry.java │ │ ├── MemoryTableVariable.java │ │ ├── MemorySnapshot.java │ │ ├── MemoryArea.java │ │ ├── Cpu.java │ │ └── Memory.java │ │ ├── datatype │ │ ├── ConverterException.java │ │ ├── DataTypeUtils.java │ │ ├── DataTypeException.java │ │ ├── BCD.java │ │ ├── ByteConverter.java │ │ ├── IntConverter.java │ │ ├── DIntConverter.java │ │ ├── WordConverter.java │ │ ├── RealConverter.java │ │ ├── S7StringConverter.java │ │ ├── DwordConverter.java │ │ ├── StringConverter.java │ │ └── DateConverter.java │ │ ├── protocol │ │ ├── Protocol.java │ │ ├── CommunicationTask.java │ │ ├── ProtocolDefinition.java │ │ ├── ResponseFactory.java │ │ └── TaskFactory.java │ │ ├── address │ │ ├── AddressParserFactory.java │ │ ├── AddressParserException.java │ │ └── ParsedAddress.java │ │ ├── executor │ │ └── ScheduledThreadPoolExecutorFactory.java │ │ ├── factory │ │ └── PlcFactoryException.java │ │ ├── view │ │ ├── error │ │ │ └── ErrorDialog.java │ │ ├── AddMemoryAreaRangeDialogController.java │ │ ├── AddMemoryAreaRangeDialog.fxml │ │ ├── Application.fxml │ │ ├── CpuTableView.fxml │ │ └── ApplicationController.java │ │ ├── serializer │ │ ├── MemoryAreaData.java │ │ └── MemorySerializer.java │ │ ├── symbol │ │ └── SymbolTableImpl.java │ │ ├── memorytables │ │ ├── MemoryTableWriteTask.java │ │ └── MemoryTableUpdateTask.java │ │ ├── cpu │ │ └── ErrorLogImpl.java │ │ ├── program │ │ └── Precompiler.java │ │ ├── comm │ │ ├── common │ │ │ ├── ServerDataEvent.java │ │ │ ├── ChangeRequest.java │ │ │ └── ClientChannelCache.java │ │ ├── tasks │ │ │ ├── AbstractCommunicationTask.java │ │ │ └── CommunicationTaskFactory.java │ │ ├── NetworkInterfaceImpl.java │ │ └── RequestWorker.java │ │ ├── classloader │ │ └── FolderClassLoader.java │ │ ├── file │ │ ├── FileUtil.java │ │ └── FileManager.java │ │ ├── plc │ │ └── PlcImpl.java │ │ ├── memory │ │ └── MemoryAreaImpl.java │ │ ├── SoftplcApplication.java │ │ ├── reflection │ │ └── AnnotationProcessor.java │ │ └── transformer │ │ └── PlcTransformer.java ├── protocols │ └── .gitignore ├── build │ ├── .gitignore │ └── resources │ │ ├── Softplc.ico │ │ └── logging.properties ├── softplc.asta ├── images │ ├── menu-file.png │ ├── menu-view.png │ ├── overview.png │ ├── about-dialog.png │ ├── softplc-cfg.png │ ├── symbol-table.png │ ├── table-cpus.png │ ├── errorlog-table.png │ ├── program-table.png │ ├── add-range-dialog.png │ ├── context-menu-cpu.png │ ├── snapshots-table.png │ ├── installation-folder.png │ ├── memory-config-table.png │ ├── memory-tables-table.png │ ├── context-menu-program.png │ ├── context-menu-snapshots.png │ ├── context-menu-memory-config.png │ ├── context-menu-memory-tables.png │ ├── context-menu-symbol-table.png │ └── context-menu-memory-variables.png ├── captures │ ├── telegrams.xlsx │ ├── readBytes.pcapng │ ├── writeBits.pcapng │ ├── invalidSlot.pcapng │ └── writeBytes.pcapng ├── .gitignore ├── build.fxbuild ├── LICENCE ├── softplc.dtd ├── pom.xml └── logging.properties └── README.md /softplc/src/test/resources/cl/file1.jar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /softplc/src/test/resources/cl/file2.jar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /softplc/src/test/resources/program/invalid.js: -------------------------------------------------------------------------------- 1 | +ü31ü+13ü -------------------------------------------------------------------------------- /softplc/src/main/resources/script/main.js: -------------------------------------------------------------------------------- 1 | main = function() { 2 | 3 | } -------------------------------------------------------------------------------- /softplc/protocols/.gitignore: -------------------------------------------------------------------------------- 1 | /connector.plc.ra.virtualplc-0.0.1-SNAPSHOT.jar 2 | -------------------------------------------------------------------------------- /softplc/build/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /deploy/ 3 | /dist/ 4 | /externalLibs/ 5 | /project/ 6 | -------------------------------------------------------------------------------- /softplc/softplc.asta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/softplc.asta -------------------------------------------------------------------------------- /softplc/src/test/resources/program/valid.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | ${"M,W100"} = 10; 3 | } 4 | -------------------------------------------------------------------------------- /softplc/images/menu-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/menu-file.png -------------------------------------------------------------------------------- /softplc/images/menu-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/menu-view.png -------------------------------------------------------------------------------- /softplc/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/overview.png -------------------------------------------------------------------------------- /softplc/captures/telegrams.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/captures/telegrams.xlsx -------------------------------------------------------------------------------- /softplc/images/about-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/about-dialog.png -------------------------------------------------------------------------------- /softplc/images/softplc-cfg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/softplc-cfg.png -------------------------------------------------------------------------------- /softplc/images/symbol-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/symbol-table.png -------------------------------------------------------------------------------- /softplc/images/table-cpus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/table-cpus.png -------------------------------------------------------------------------------- /softplc/captures/readBytes.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/captures/readBytes.pcapng -------------------------------------------------------------------------------- /softplc/captures/writeBits.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/captures/writeBits.pcapng -------------------------------------------------------------------------------- /softplc/images/errorlog-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/errorlog-table.png -------------------------------------------------------------------------------- /softplc/images/program-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/program-table.png -------------------------------------------------------------------------------- /softplc/build/resources/Softplc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/build/resources/Softplc.ico -------------------------------------------------------------------------------- /softplc/captures/invalidSlot.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/captures/invalidSlot.pcapng -------------------------------------------------------------------------------- /softplc/captures/writeBytes.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/captures/writeBytes.pcapng -------------------------------------------------------------------------------- /softplc/images/add-range-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/add-range-dialog.png -------------------------------------------------------------------------------- /softplc/images/context-menu-cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-cpu.png -------------------------------------------------------------------------------- /softplc/images/snapshots-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/snapshots-table.png -------------------------------------------------------------------------------- /softplc/images/installation-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/installation-folder.png -------------------------------------------------------------------------------- /softplc/images/memory-config-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/memory-config-table.png -------------------------------------------------------------------------------- /softplc/images/memory-tables-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/memory-tables-table.png -------------------------------------------------------------------------------- /softplc/images/context-menu-program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-program.png -------------------------------------------------------------------------------- /softplc/images/context-menu-snapshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-snapshots.png -------------------------------------------------------------------------------- /softplc/src/test/resources/file1.snapshot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/src/test/resources/file1.snapshot -------------------------------------------------------------------------------- /softplc/images/context-menu-memory-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-memory-config.png -------------------------------------------------------------------------------- /softplc/images/context-menu-memory-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-memory-tables.png -------------------------------------------------------------------------------- /softplc/images/context-menu-symbol-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-symbol-table.png -------------------------------------------------------------------------------- /softplc/images/context-menu-memory-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/images/context-menu-memory-variables.png -------------------------------------------------------------------------------- /softplc/src/main/resources/images/softplc_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteral/softplc/HEAD/softplc/src/main/resources/images/softplc_32.png -------------------------------------------------------------------------------- /softplc/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /softplc.asta.bak 3 | /.classpath 4 | /.project 5 | /.settings/ 6 | /dependency-reduced-pom.xml 7 | /softplc.log.* 8 | /.idea 9 | /softplc.iml -------------------------------------------------------------------------------- /softplc/src/test/resources/program/main.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | ${"M,W100"} = ${"M,W100"}.intValue() + 1; 3 | 4 | logger.info("New value: " + ${"M,W100"}); 5 | } 6 | -------------------------------------------------------------------------------- /softplc/src/test/resources/composite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/reflection/TestInterface.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.reflection; 2 | 3 | @SuppressWarnings("javadoc") 4 | public interface TestInterface { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /softplc/src/test/resources/program/main_translated.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | memory.write("M,W100", memory.read("M,W100").intValue() + 1); 3 | 4 | logger.info("New value: " + memory.read("M,W100")); 5 | } 6 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/reflection/TestClass1.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.reflection; 2 | 3 | @SuppressWarnings("javadoc") 4 | @TestAnnotation(priority = 10) 5 | public class TestClass1 implements TestInterface { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/reflection/TestClass2.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.reflection; 2 | 3 | @SuppressWarnings("javadoc") 4 | @TestAnnotation(priority = 20) 5 | public class TestClass2 implements TestInterface { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/PutGetServerEvent.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Contains information about one interaction between {@link NetworkInterface} and a 5 | * client. 6 | *

7 | * Implement {@link PutGetServerObserver} in order to receive this information. 8 | * 9 | * @author peteral 10 | * 11 | */ 12 | public class PutGetServerEvent { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/ConverterException.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | /** 4 | * @author peteral 5 | * 6 | */ 7 | public class ConverterException extends RuntimeException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | /** 11 | * Create new instance. 12 | * 13 | * @param message 14 | */ 15 | public ConverterException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/CpuStatus.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Defines the possible states the {@link Cpu} can enter. 5 | * 6 | * @author peteral 7 | * 8 | */ 9 | public enum CpuStatus { 10 | /** 11 | * Invalid program. 12 | */ 13 | ERROR, 14 | 15 | /** 16 | * Running 17 | */ 18 | RUN, 19 | 20 | /** 21 | * Stopped - program execution interrupted by user 22 | */ 23 | STOP; 24 | } 25 | -------------------------------------------------------------------------------- /softplc/build.fxbuild: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/reflection/TestAnnotation.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.reflection; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @SuppressWarnings("javadoc") 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target(ElementType.TYPE) 11 | public @interface TestAnnotation { 12 | int priority() default 0; 13 | } 14 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/DataTypeUtils.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | /** 4 | * Data type utilities. 5 | * 6 | * @author peteral 7 | * 8 | */ 9 | public final class DataTypeUtils { 10 | 11 | private DataTypeUtils() { 12 | 13 | } 14 | 15 | /** 16 | * Converts unsigned 8 bit stored within byte into integer value. 17 | * 18 | * @param b 19 | * @return integer 20 | */ 21 | public static int byteToInt(byte b) { 22 | return (b < 0) ? 128 + (b & 0x7F) : b; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/protocol/Protocol.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.protocol; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a class as a protocol definition. Must implement 10 | * {@link ProtocolDefinition} interface, 11 | * 12 | * @author peteral 13 | */ 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface Protocol { 17 | } 18 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/address/AddressParserFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.address; 2 | 3 | /** 4 | * {@link ParsedAddress} factory for easier unit testing. 5 | * 6 | * @author peteral 7 | * 8 | */ 9 | public class AddressParserFactory { 10 | 11 | /** 12 | * Parses the address. 13 | * 14 | * @param address 15 | * address to be parsed. 16 | * @return {@link ParsedAddress} instance according the address. 17 | */ 18 | public ParsedAddress parse(String address) { 19 | return new ParsedAddress(address); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/PutGetServerObserver.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Classes implementing this interface can register with the 5 | * {@link NetworkInterface} and receive information about data exchange with 6 | * clients. 7 | * 8 | * @author peteral 9 | * 10 | */ 11 | public interface PutGetServerObserver { 12 | /** 13 | * Invoked after each processed telegram processed. 14 | * 15 | * @param e 16 | * event containing information about the data exchange. 17 | */ 18 | void onTelegram(PutGetServerEvent e); 19 | } 20 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/address/AddressParserException.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.address; 2 | 3 | /** 4 | * This exception is thrown by the {@link AddressParserFactory} in case of an 5 | * invalid address. 6 | * 7 | * @author peteral 8 | */ 9 | public class AddressParserException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | /** 14 | * Creates a new instance. 15 | *

16 | * 17 | * @param address 18 | * requested address 19 | */ 20 | public AddressParserException(String address) { 21 | super("Invalid address: [" + address + "]"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/SoftplcResponseFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a class as a response factory. Must implement {@link ResponseFactory} 10 | * interface, 11 | * 12 | * @author peteral 13 | */ 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface SoftplcResponseFactory { 17 | /** 18 | * 19 | * @return priority of this factory 0 = lowest 20 | */ 21 | int priority() default 0; 22 | } 23 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/executor/ScheduledThreadPoolExecutorFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.executor; 2 | 3 | import java.util.concurrent.ScheduledThreadPoolExecutor; 4 | 5 | /** 6 | * An shut down executor cannot be reused. We need to create a new instance upon 7 | * every CPU start. 8 | *

9 | * This factory allows testability. 10 | * 11 | * @author peteral 12 | * 13 | */ 14 | public class ScheduledThreadPoolExecutorFactory { 15 | 16 | /** 17 | * 18 | * @return new {@link ScheduledThreadPoolExecutor} for cpu execution 19 | */ 20 | public ScheduledThreadPoolExecutor createExecutor() { 21 | return new ScheduledThreadPoolExecutor(1); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/DataTypeException.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | /** 4 | * Exception thrown by {@link DataTypeFactory} when invalid data type is 5 | * requested. 6 | * 7 | * @author peteral 8 | * 9 | */ 10 | public class DataTypeException extends RuntimeException { 11 | 12 | private static final long serialVersionUID = 1L; 13 | 14 | /** 15 | * Creates new instance. 16 | * 17 | * @param typeName 18 | * name of the requested type 19 | * @param message 20 | * additional message 21 | */ 22 | public DataTypeException(String typeName, String message) { 23 | super("Data type error [" + typeName + "]: " + message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/CommunicationTask.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Defines a task created by {@link PutGetServer} to be executed by {@link Cpu} 5 | * between two {@link Program} cycles. 6 | * 7 | * @author peteral 8 | * 9 | */ 10 | public interface CommunicationTask { 11 | 12 | /** 13 | * This method will be invoked by the {@link Cpu} 14 | * 15 | * @param cpu 16 | * {@link Cpu} instance executing this task (for memory 17 | * interaction) 18 | */ 19 | void execute(Cpu cpu); 20 | 21 | /** 22 | * Invoked when task requests invalid CPU slot 23 | * 24 | * @param slot 25 | * requested cpu slot 26 | */ 27 | void onInvalidCpu(int slot); 28 | } 29 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/MemoryAccessViolationException.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import de.peteral.softplc.protocol.CommunicationTask; 4 | 5 | /** 6 | * This exception is thrown when the {@link Program} or a 7 | * {@link CommunicationTask} attempts to access invalid memory area. 8 | * 9 | * @author peteral 10 | * 11 | */ 12 | public class MemoryAccessViolationException extends RuntimeException { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | /** 17 | * Creates new instance. 18 | * 19 | * @param message 20 | * message describing the origin of this exception. 21 | */ 22 | public MemoryAccessViolationException(String message) { 23 | super(message); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/SymbolTable.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import javafx.collections.ObservableList; 4 | 5 | /** 6 | * This interface defines a symbol table of a CPU. 7 | *

8 | * A symbol table translates symbolic names to physical addresses. 9 | * 10 | * @author peteral 11 | * 12 | */ 13 | public interface SymbolTable { 14 | /** 15 | * 16 | * @return observable list of all symbols - elements can be added, removed 17 | * and modified 18 | */ 19 | ObservableList getAllSymbols(); 20 | 21 | /** 22 | * Translates symbolic name to an address 23 | * 24 | * @param name 25 | * symbolic name 26 | * @return hardware address, null when symbol is not defined 27 | */ 28 | String getAddress(String name); 29 | } 30 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/BCD.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | /** 4 | * BCD conversion utility class. 5 | * 6 | * @author peteral 7 | * 8 | */ 9 | public class BCD { 10 | 11 | /** 12 | * Converts decimal value to BCD code. 13 | * 14 | * @param value 15 | * only handled as byte! 16 | * @return BCD coded value 17 | */ 18 | public static byte toBCD(int value) { 19 | int low = value % 10; 20 | int high = value / 10; 21 | return (byte) ((high << 4) + low); 22 | } 23 | 24 | /** 25 | * Converts BCD coded value to decimal. 26 | * 27 | * @param bcd 28 | * @return decimal value 29 | */ 30 | public static int fromBCD(byte bcd) { 31 | int high = (bcd & 0xf0) >> 4; 32 | int low = bcd & 0x0f; 33 | 34 | return (10 * high) + low; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/ProgramCycleObserver.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Classes implementing this interface can register to be informed about the 5 | * life cycle of a {@link Program} execution. 6 | * 7 | * @author peteral 8 | */ 9 | public interface ProgramCycleObserver { 10 | /** 11 | * Invoked after a program cycle was finished. 12 | */ 13 | void afterCycleEnd(); 14 | 15 | /** 16 | * Invoked in case of error just before the exception would be thrown. 17 | * 18 | * @param context 19 | * context in which the error happened - for extended logging 20 | * 21 | * @param e 22 | * exception about to be thrown 23 | * @return false - exception has been handled, do not throw it 24 | */ 25 | boolean onError(String context, Throwable e); 26 | } 27 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/protocol/CommunicationTask.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.protocol; 2 | 3 | import de.peteral.softplc.model.Cpu; 4 | import de.peteral.softplc.model.Program; 5 | import de.peteral.softplc.model.NetworkInterface; 6 | 7 | /** 8 | * Defines a task created by {@link NetworkInterface} to be executed by {@link Cpu} 9 | * between two {@link Program} cycles. 10 | * 11 | * @author peteral 12 | * 13 | */ 14 | public interface CommunicationTask { 15 | 16 | /** 17 | * This method will be invoked by the {@link Cpu} 18 | * 19 | * @param cpu 20 | * {@link Cpu} instance executing this task (for memory 21 | * interaction) 22 | */ 23 | void execute(Cpu cpu); 24 | 25 | /** 26 | * Invoked when task requests invalid CPU slot 27 | * 28 | * @param slot 29 | * requested cpu slot 30 | */ 31 | void onInvalidCpu(int slot); 32 | } 33 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/classloader/FolderClassLoaderTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.classloader; 2 | 3 | import static org.junit.Assert.assertArrayEquals; 4 | 5 | import java.io.File; 6 | import java.net.URISyntaxException; 7 | import java.net.URL; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | @SuppressWarnings("javadoc") 13 | public class FolderClassLoaderTest { 14 | 15 | private FolderClassLoader classLoader; 16 | 17 | @Before 18 | public void setup() throws URISyntaxException { 19 | classLoader = new FolderClassLoader(new File(getClass().getResource("/cl").toURI())); 20 | } 21 | 22 | @Test 23 | public void getURLs_FolderWith2JarFiles_ReturnsCorrectURLs() { 24 | URL[] urls = classLoader.getURLs(); 25 | 26 | assertArrayEquals(urls, 27 | new URL[] { getClass().getResource("/cl/file1.jar"), getClass().getResource("/cl/file2.jar"), }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | softplc 2 | ======= 3 | 4 | Simulation of a PLC for integration testing of SCADA systems. Originally planned to make a simulation of Siemens Simatic PLC. However over time decided to make a generic PLC. Network protocols are pluggable via @Protocol annotation. 5 | 6 | My company runs an internal fork with S7 protocol implementation. Planning to implement at least modbus as reference protocol implementation. 7 | 8 | Current functionality: 9 | - multiple configurable CPUs per PLC 10 | - each CPU executes a JavaScript program 11 | - serves pluggable network protocols 12 | - manual access to memory via "variable tables" 13 | - GUI 14 | 15 | See wiki for more information and docs: https://github.com/peteral/softplc/wiki 16 | 17 | Licence: http://opensource.org/licenses/MIT 18 | 19 | Special thanks to: 20 | - https://github.com/ 21 | - http://sourceforge.net/projects/libnodave/ 22 | - https://www.wireshark.org/ 23 | - http://astah.net/editions/community -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/ErrorLog.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.util.logging.Level; 4 | 5 | import javafx.collections.ObservableList; 6 | 7 | /** 8 | * Each {@link Cpu} posesses an {@link ErrorLog} instance. This is used by 9 | * {@link Memory} and {@link Program} to log problems and helps locate errors in 10 | * user programs. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public interface ErrorLog { 16 | 17 | /** 18 | * Logs a message. 19 | * 20 | * @param level 21 | * log level 22 | * @param module 23 | * module reporting the error 24 | * @param message 25 | * message text 26 | */ 27 | void log(Level level, String module, String message); 28 | 29 | /** 30 | * 31 | * @return observable list of last n logged entries for this CPU 32 | */ 33 | ObservableList getEntries(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/ResponseFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | /** 4 | * Creates a byte array to be sent back to a client from a 5 | * {@link CommunicationTask} instance. 6 | * 7 | * @author peteral 8 | * 9 | */ 10 | public interface ResponseFactory { 11 | /** 12 | * Signals whether this factory is able to handle the concrete 13 | * {@link CommunicationTask}. 14 | * 15 | * @param task 16 | * task to be checked 17 | * @return true - this {@link ResponseFactory} instance can handle the task 18 | */ 19 | boolean canHandle(CommunicationTask task); 20 | 21 | /** 22 | * Creates a byte array to be sent back to a client from a 23 | * {@link CommunicationTask} instance. 24 | * 25 | * @param task 26 | * communication task 27 | * @return byte array to be sent back to a client 28 | */ 29 | byte[] createResponse(CommunicationTask task); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/reflection/AnnotationProcessorTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.reflection; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import org.junit.Test; 10 | import org.reflections.util.ClasspathHelper; 11 | 12 | @SuppressWarnings("javadoc") 13 | public class AnnotationProcessorTest { 14 | 15 | @Test 16 | public void loadAnnotations_TwoClassesInClasspath_ReturnsInstancesInCorrectOrder() { 17 | List result = new ArrayList<>(); 18 | new AnnotationProcessor(TestAnnotation.class, 19 | ClasspathHelper.forClass(AnnotationProcessorTest.class)) 20 | .loadAnnotations(result); 21 | 22 | assertEquals(2, result.size()); 23 | 24 | assertTrue(result.get(0) instanceof TestClass2); 25 | assertTrue(result.get(1) instanceof TestClass1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/MemoryTable.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import javafx.beans.property.SimpleStringProperty; 4 | import javafx.beans.property.StringProperty; 5 | import javafx.collections.FXCollections; 6 | import javafx.collections.ObservableList; 7 | 8 | /** 9 | * Memory tables can be configured in UI to observe and interact with PLC 10 | * memory. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public class MemoryTable { 16 | private final StringProperty name = new SimpleStringProperty(); 17 | 18 | private final ObservableList variables = FXCollections 19 | .observableArrayList(); 20 | 21 | /** 22 | * @return the variables 23 | */ 24 | public ObservableList getVariables() { 25 | return variables; 26 | } 27 | 28 | /** 29 | * @return the name 30 | */ 31 | public StringProperty getName() { 32 | return name; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/protocol/ProtocolDefinition.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.protocol; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Defines a communication protocol. 7 | * 8 | * @author peteral 9 | * 10 | */ 11 | public interface ProtocolDefinition { 12 | /** 13 | * 14 | * @return array containing a list of communication task factories of this 15 | * protocol (byte array -> command) 16 | */ 17 | List getTaskFactories(); 18 | 19 | /** 20 | * 21 | * @return array containing list of response factories (executed command -> 22 | * byte array) 23 | */ 24 | List getResponseFactories(); 25 | 26 | /** 27 | * 28 | * @return array containing list of ports this protocol is bound to 29 | */ 30 | List getPorts(); 31 | 32 | /** 33 | * 34 | * @return protocol name (for showing active protocols) 35 | */ 36 | String getName(); 37 | } 38 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/protocol/ResponseFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.protocol; 2 | 3 | /** 4 | * Creates a byte array to be sent back to a client from a 5 | * {@link CommunicationTask} instance. 6 | * 7 | * @author peteral 8 | * 9 | */ 10 | public interface ResponseFactory { 11 | /** 12 | * Signals whether this factory is able to handle the concrete 13 | * {@link CommunicationTask}. 14 | * 15 | * @param task 16 | * task to be checked 17 | * @return true - this {@link ResponseFactory} instance can handle the task 18 | */ 19 | boolean canHandle(CommunicationTask task); 20 | 21 | /** 22 | * Creates a byte array to be sent back to a client from a 23 | * {@link CommunicationTask} instance. 24 | * 25 | * @param task 26 | * communication task 27 | * @return byte array to be sent back to a client 28 | */ 29 | byte[] createResponse(CommunicationTask task); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/symbol/SymbolTableImplTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.symbol; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNull; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import de.peteral.softplc.model.Symbol; 10 | 11 | @SuppressWarnings("javadoc") 12 | public class SymbolTableImplTest { 13 | 14 | private static final String SYMBOL = "symbol"; 15 | private static final String ADDRESS = "address"; 16 | private SymbolTableImpl table; 17 | 18 | @Before 19 | public void setup() { 20 | table = new SymbolTableImpl(); 21 | } 22 | 23 | @Test 24 | public void getAddress_SymbolDefined_ReturnsAddress() { 25 | table.getAllSymbols().add(new Symbol(SYMBOL, ADDRESS)); 26 | 27 | assertEquals(ADDRESS, table.getAddress(SYMBOL)); 28 | } 29 | 30 | @Test 31 | public void getAddress_SymbolUndefined_ReturnsNull() { 32 | assertNull(table.getAddress(SYMBOL)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/factory/PlcFactoryException.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.factory; 2 | 3 | /** 4 | * This exception is thrown by {@link PlcFactory} in case of an invalid 5 | * configuration file. 6 | * 7 | * @author peteral 8 | * 9 | */ 10 | public class PlcFactoryException extends RuntimeException { 11 | 12 | private static final long serialVersionUID = 1L; 13 | 14 | /** 15 | * Creates a new instance. 16 | * 17 | * @param path 18 | * configuration file 19 | * @param reason 20 | * causing exception 21 | */ 22 | public PlcFactoryException(String path, Throwable reason) { 23 | super("Failed parsing configuration [" + path + "]: ", reason); 24 | } 25 | 26 | /** 27 | * Creates instance with additional information. 28 | * 29 | * @param path 30 | * @param reason 31 | */ 32 | public PlcFactoryException(String path, String reason) { 33 | super("Failed parsing configuration [" + path + "]: " + reason); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/view/error/ErrorDialog.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.view.error; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | import javafx.scene.control.Alert; 7 | import javafx.scene.control.Alert.AlertType; 8 | 9 | /** 10 | * Dialog for displaying exceptions. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public final class ErrorDialog { 16 | 17 | private ErrorDialog() { 18 | } 19 | 20 | /** 21 | * Shows the error dialog 22 | * 23 | * @param message 24 | * additional message 25 | * @param reason 26 | * error description 27 | */ 28 | public static void show(String message, Throwable reason) { 29 | Logger.getLogger("global").log(Level.WARNING, message, reason); 30 | 31 | Alert alert = new Alert(AlertType.ERROR); 32 | alert.setTitle("Error"); 33 | alert.setHeaderText(message); 34 | alert.setContentText((reason == null) ? "" : reason.getMessage()); 35 | 36 | alert.showAndWait(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/serializer/MemoryAreaData.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.serializer; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Serializable memory area reprezentation to be saved in a memory snapshot. 7 | * 8 | * @author peteral 9 | * 10 | */ 11 | public class MemoryAreaData implements Serializable { 12 | 13 | private static final long serialVersionUID = 1L; 14 | private final String areaCode; 15 | private final byte[] bytes; 16 | 17 | /** 18 | * Default constructor 19 | * 20 | * @param areaCode 21 | * memory area name 22 | * @param bytes 23 | * memory area content 24 | */ 25 | public MemoryAreaData(String areaCode, byte[] bytes) { 26 | this.areaCode = areaCode; 27 | this.bytes = bytes; 28 | } 29 | 30 | /** 31 | * @return the areaCode 32 | */ 33 | public String getAreaCode() { 34 | return areaCode; 35 | } 36 | 37 | /** 38 | * @return the bytes 39 | */ 40 | public byte[] getBytes() { 41 | return bytes; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/ByteConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converter implementation for byte (8 bit unsigned) conversion. 8 | * 9 | * @author peteral 10 | */ 11 | public class ByteConverter implements Converter { 12 | 13 | @Override 14 | public Number[] createArray(int count) { 15 | return new Integer[count]; 16 | } 17 | 18 | @Override 19 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 20 | int offset) { 21 | 22 | buffer[offset] = value.byteValue(); 23 | } 24 | 25 | @Override 26 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 27 | return Integer.valueOf(DataTypeUtils.byteToInt(bytes[offset])); 28 | } 29 | 30 | @Override 31 | public void parseToBytes(String value, ParsedAddress address, 32 | byte[] buffer, int offset) { 33 | 34 | toBytes(Integer.parseInt(value), address, buffer, offset); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/symbol/SymbolTableImpl.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.symbol; 2 | 3 | import de.peteral.softplc.model.Symbol; 4 | import de.peteral.softplc.model.SymbolTable; 5 | import javafx.collections.FXCollections; 6 | import javafx.collections.ObservableList; 7 | 8 | /** 9 | * {@link SymbolTable} implementation. 10 | *

11 | * not thread safe 12 | * 13 | * @author peteral 14 | * 15 | */ 16 | // TODO - possible optimization - listen to symbols list changes and update a 17 | // map, use this map to resolve addresses 18 | public class SymbolTableImpl implements SymbolTable { 19 | private final ObservableList symbols = FXCollections.observableArrayList(); 20 | 21 | @Override 22 | public ObservableList getAllSymbols() { 23 | return symbols; 24 | } 25 | 26 | @Override 27 | public String getAddress(String name) { 28 | for (Symbol symbol : symbols) { 29 | if (symbol.getName().get().equals(name)) { 30 | return symbol.getAddress().get(); 31 | } 32 | } 33 | 34 | return null; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/main/JavascriptTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.main; 2 | 3 | import javax.script.Bindings; 4 | import javax.script.Compilable; 5 | import javax.script.CompiledScript; 6 | import javax.script.ScriptContext; 7 | import javax.script.ScriptEngine; 8 | import javax.script.ScriptEngineManager; 9 | import javax.script.ScriptException; 10 | import javax.script.SimpleScriptContext; 11 | 12 | @SuppressWarnings("javadoc") 13 | public class JavascriptTest { 14 | 15 | public static void main(String[] args) throws ScriptException { 16 | ScriptEngineManager manager = new ScriptEngineManager(); 17 | 18 | ScriptEngine engine = manager.getEngineByMimeType("text/javascript"); 19 | Compilable compiler = (Compilable) engine; 20 | 21 | CompiledScript compiledScript = compiler.compile("a = 20;"); 22 | 23 | ScriptContext context = new SimpleScriptContext(); 24 | Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); 25 | 26 | compiledScript.eval(context); 27 | 28 | System.out.println(bindings.get("a")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/comm/common/ChangeRequestTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.common; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.nio.channels.SocketChannel; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | @SuppressWarnings("javadoc") 13 | public class ChangeRequestTest { 14 | private static final int TYPE = 10; 15 | private static final int OPS = 20; 16 | @Mock 17 | private SocketChannel socket; 18 | private ChangeRequest request; 19 | 20 | @Before 21 | public void setup() { 22 | MockitoAnnotations.initMocks(this); 23 | 24 | request = new ChangeRequest(socket, TYPE, OPS); 25 | } 26 | 27 | @Test 28 | public void getSocket_None_ReturnsSocket() { 29 | assertEquals(socket, request.getSocket()); 30 | } 31 | 32 | @Test 33 | public void getType_None_ReturnsType() { 34 | assertEquals(TYPE, request.getType()); 35 | } 36 | 37 | @Test 38 | public void getOps_None_ReturnsOps() { 39 | assertEquals(OPS, request.getOps()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/memorytables/MemoryTableWriteTask.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.memorytables; 2 | 3 | import java.util.Arrays; 4 | 5 | import de.peteral.softplc.model.Cpu; 6 | import de.peteral.softplc.model.MemoryTableVariable; 7 | import de.peteral.softplc.protocol.CommunicationTask; 8 | 9 | /** 10 | * Writes memory table variables to memory. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public class MemoryTableWriteTask implements CommunicationTask { 16 | 17 | private final MemoryTableVariable[] variables; 18 | 19 | /** 20 | * Initializes new instance. 21 | * 22 | * @param variables 23 | * variables to be written 24 | */ 25 | public MemoryTableWriteTask(MemoryTableVariable... variables) { 26 | this.variables = variables; 27 | } 28 | 29 | @Override 30 | public void execute(Cpu cpu) { 31 | Arrays.asList(variables).forEach( 32 | variable -> cpu.getMemory().parse(variable.getVariable().get(), 33 | variable.getNewValue().get())); 34 | } 35 | 36 | @Override 37 | public void onInvalidCpu(int slot) { 38 | // nothing to do here 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/cpu/ErrorLogImpl.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.cpu; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | import javafx.collections.FXCollections; 7 | import javafx.collections.ObservableList; 8 | import de.peteral.softplc.model.ErrorLog; 9 | import de.peteral.softplc.model.ErrorLogEntry; 10 | 11 | /** 12 | * Default {@link ErrorLog} implementation. 13 | * 14 | * @author peteral 15 | * 16 | */ 17 | public class ErrorLogImpl implements ErrorLog { 18 | private static final int MAX_ENTRIES = 300; 19 | 20 | private final ObservableList entries = FXCollections 21 | .observableArrayList(); 22 | 23 | @Override 24 | public void log(Level level, String module, String message) { 25 | getEntries().add(new ErrorLogEntry(level, module, message)); 26 | 27 | while (getEntries().size() > MAX_ENTRIES) { 28 | getEntries().remove(0); 29 | } 30 | 31 | Logger.getLogger(module).log(level, module + ": " + message); 32 | } 33 | 34 | @Override 35 | public ObservableList getEntries() { 36 | return entries; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /softplc/LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ladislav Petera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/IntConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converts signed 16-bit integer. 8 | * 9 | * @author peteral 10 | */ 11 | public class IntConverter implements Converter { 12 | private final WordConverter wordConverter = new WordConverter(); 13 | 14 | @Override 15 | public Number[] createArray(int count) { 16 | return new Short[count]; 17 | } 18 | 19 | @Override 20 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 21 | int offset) { 22 | wordConverter.toBytes(value, address, buffer, offset); 23 | } 24 | 25 | @Override 26 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 27 | int intValue = (int) wordConverter.fromBytes(bytes, address, offset); 28 | 29 | return (short) intValue; 30 | } 31 | 32 | @Override 33 | public void parseToBytes(String value, ParsedAddress address, 34 | byte[] buffer, int offset) { 35 | 36 | toBytes(Short.parseShort(value), address, buffer, offset); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/Symbol.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import javafx.beans.property.SimpleStringProperty; 4 | import javafx.beans.property.StringProperty; 5 | 6 | /** 7 | * This class represents an entry in the symbol table of a CPU. 8 | * 9 | * @author peteral 10 | * 11 | */ 12 | public class Symbol { 13 | private final StringProperty address = new SimpleStringProperty(); 14 | private final StringProperty name = new SimpleStringProperty(); 15 | 16 | /** 17 | * Default constructor. 18 | * 19 | * @param name 20 | * symbolic name 21 | * @param address 22 | * address 23 | */ 24 | public Symbol(String name, String address) { 25 | getAddress().set(address); 26 | getName().set(name); 27 | } 28 | 29 | /** 30 | * @return the address 31 | */ 32 | public StringProperty getAddress() { 33 | return address; 34 | } 35 | 36 | /** 37 | * @return the name 38 | */ 39 | public StringProperty getName() { 40 | return name; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return getName().get() + " = " + getAddress().get(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/protocol/TaskFactory.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.protocol; 2 | 3 | import de.peteral.softplc.comm.common.ServerDataEvent; 4 | import de.peteral.softplc.comm.tasks.CommunicationTaskFactory; 5 | 6 | /** 7 | * TaskFactory implementations create {@link CommunicationTask} from 8 | * {@link ServerDataEvent}. 9 | * 10 | * @author peteral 11 | * 12 | */ 13 | public interface TaskFactory { 14 | /** 15 | * Signals whether this factory instance can handle the event. 16 | * 17 | * @param dataEvent 18 | * event that needs to be translated into 19 | * {@link CommunicationTask} 20 | * @return true - this instance can handle the event 21 | */ 22 | boolean canHandle(ServerDataEvent dataEvent); 23 | 24 | /** 25 | * Creates a concrete {@link CommunicationTask} instance from a 26 | * {@link ServerDataEvent}. 27 | * 28 | * @param dataEvent 29 | * server data event 30 | * @param factory 31 | * @return concrete {@link CommunicationTask} instance 32 | */ 33 | CommunicationTask createTask(ServerDataEvent dataEvent, 34 | CommunicationTaskFactory factory); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /softplc/src/test/resources/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ./program/main.js 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./program/main.js 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
-------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/DIntConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converter handling 32 bit signed decimals. 8 | * 9 | * @author peteral 10 | */ 11 | public class DIntConverter implements Converter { 12 | private final DwordConverter dwordConverter = new DwordConverter(); 13 | 14 | @Override 15 | public Number[] createArray(int count) { 16 | return new Integer[count]; 17 | } 18 | 19 | @Override 20 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 21 | int offset) { 22 | dwordConverter.toBytes(value, address, buffer, offset); 23 | } 24 | 25 | @Override 26 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 27 | long longValue = (long) dwordConverter 28 | .fromBytes(bytes, address, offset); 29 | return (int) longValue; 30 | } 31 | 32 | @Override 33 | public void parseToBytes(String value, ParsedAddress address, 34 | byte[] buffer, int offset) { 35 | 36 | toBytes(Long.parseLong(value), address, buffer, offset); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/program/PrecompilerTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.program; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Paths; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import de.peteral.softplc.program.Precompiler; 14 | 15 | @SuppressWarnings("javadoc") 16 | public class PrecompilerTest { 17 | 18 | private Precompiler precompiler; 19 | 20 | @Before 21 | public void setup() { 22 | MockitoAnnotations.initMocks(this); 23 | 24 | precompiler = new Precompiler(); 25 | } 26 | 27 | @Test 28 | public void translate_ValidFile_ReturnsCorrectScript() throws IOException { 29 | String input = readFile("./src/test/resources/program/main.js"); 30 | String output = readFile("./src/test/resources/program/main_translated.js"); 31 | 32 | assertEquals(output, precompiler.translate(input)); 33 | } 34 | 35 | private String readFile(String filename) throws IOException { 36 | byte[] bytes = Files.readAllBytes(Paths.get(filename)); 37 | return new String(bytes); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/datatype/BCDTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.Arrays; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.Parameterized; 10 | import org.junit.runners.Parameterized.Parameters; 11 | 12 | @SuppressWarnings({ "javadoc" }) 13 | @RunWith(Parameterized.class) 14 | public class BCDTest { 15 | 16 | private final byte bcd; 17 | private final int value; 18 | 19 | /* @formatter:off */ 20 | @Parameters(name = "{index}: {0} = {1}") 21 | public static Iterable getParameters() { 22 | return Arrays.asList(new Object[][] {// 23 | {(byte)0x21, 21 }, 24 | {(byte)0x00, 00 }, 25 | {(byte)0x17, 17 }, 26 | }); 27 | } 28 | /* @formatter:on */ 29 | 30 | public BCDTest(byte bcd, int value) { 31 | this.bcd = bcd; 32 | this.value = value; 33 | } 34 | 35 | @Test 36 | public void toBCD_Parameterized_ReturnsCorrectValue() { 37 | assertEquals(bcd, BCD.toBCD(value)); 38 | } 39 | 40 | @Test 41 | public void fromBCD_Parameterized_ReturnsCorrectValue() { 42 | assertEquals(value, BCD.fromBCD(bcd)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/WordConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converts 16-Bit unsigned number to Integer. 8 | *

9 | * First byte is MSB. 10 | * 11 | * @author peteral 12 | */ 13 | public class WordConverter implements Converter { 14 | 15 | @Override 16 | public Number[] createArray(int count) { 17 | return new Integer[count]; 18 | } 19 | 20 | @Override 21 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 22 | int offset) { 23 | 24 | buffer[offset + 1] = (byte) (value.intValue() & 0xFF); 25 | buffer[offset + 0] = (byte) ((value.intValue() & 0xFF00) >> 8); 26 | } 27 | 28 | @Override 29 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 30 | return (DataTypeUtils.byteToInt(bytes[offset]) << 8) 31 | + DataTypeUtils.byteToInt(bytes[offset + 1]); 32 | } 33 | 34 | @Override 35 | public void parseToBytes(String value, ParsedAddress address, 36 | byte[] buffer, int offset) { 37 | 38 | toBytes(Integer.parseInt(value), address, buffer, offset); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/RealConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converter handling 32 bit float. 8 | * 9 | * @author peteral 10 | */ 11 | public class RealConverter implements Converter { 12 | private final DwordConverter dwordConverter = new DwordConverter(); 13 | 14 | @Override 15 | public Number[] createArray(int count) { 16 | return new Float[count]; 17 | } 18 | 19 | @Override 20 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 21 | int offset) { 22 | dwordConverter.toBytes(Float.floatToIntBits(value.floatValue()), 23 | address, buffer, offset); 24 | } 25 | 26 | @Override 27 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 28 | Number dw = dwordConverter.fromBytes(bytes, address, offset); 29 | 30 | return Float.intBitsToFloat((int) dw.longValue()); 31 | } 32 | 33 | @Override 34 | public void parseToBytes(String value, ParsedAddress address, 35 | byte[] buffer, int offset) { 36 | 37 | toBytes(Float.parseFloat(value), address, buffer, offset); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/program/Precompiler.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.program; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * Translates programs into pure executable JavaScript. 9 | * 10 | * @author peteral 11 | */ 12 | public class Precompiler { 13 | private static final List> OPERATIONS = new ArrayList<>(); 14 | /* @formatter:off */ 15 | static 16 | { 17 | // first replace all write accesses (start of line) before assignment 18 | OPERATIONS.add(s -> s.replaceAll("(\\s*)\\$\\{([^}]*)}\\s*\\=\\s*([^;]*);", "$1memory.write($2, $3);")); 19 | // what remains are read accesses 20 | OPERATIONS.add(s -> s.replaceAll("\\$\\{([^}]*)}", "memory.read($1)")); 21 | } 22 | /* @formatter:on */ 23 | 24 | /** 25 | * Translates program with memory access tags into executable javascript. 26 | * 27 | * @param input 28 | * @return translated script. 29 | */ 30 | public String translate(String input) { 31 | String result = input; 32 | 33 | for (Function operation : OPERATIONS) { 34 | result = operation.apply(result); 35 | } 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/S7StringConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converts S7 string to / from byte arrays. 8 | *

9 | * Structure: 10 | *

15 | * 16 | * @author peteral 17 | * 18 | */ 19 | public class S7StringConverter implements Converter { 20 | 21 | @Override 22 | public String[] createArray(int count) { 23 | // TODO Auto-generated method stub 24 | return null; 25 | } 26 | 27 | @Override 28 | public void toBytes(String value, ParsedAddress address, byte[] buffer, 29 | int offset) { 30 | // TODO Auto-generated method stub 31 | 32 | } 33 | 34 | @Override 35 | public String fromBytes(byte[] bytes, ParsedAddress address, int offset) { 36 | // TODO Auto-generated method stub 37 | return null; 38 | } 39 | 40 | @Override 41 | public void parseToBytes(String value, ParsedAddress address, 42 | byte[] buffer, int offset) { 43 | // TODO Auto-generated method stub 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /softplc/softplc.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /softplc/src/test/resources/softplc.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/comm/common/ServerDataEventTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.common; 2 | 3 | import static org.junit.Assert.assertArrayEquals; 4 | import static org.junit.Assert.assertEquals; 5 | 6 | import java.nio.channels.SocketChannel; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import de.peteral.softplc.model.NetworkInterface; 14 | 15 | @SuppressWarnings("javadoc") 16 | public class ServerDataEventTest { 17 | private static final byte[] DATA = new byte[] { 0x01, 0x02 }; 18 | @Mock 19 | private NetworkInterface server; 20 | @Mock 21 | private SocketChannel socket; 22 | private ServerDataEvent event; 23 | 24 | @Before 25 | public void setup() { 26 | MockitoAnnotations.initMocks(this); 27 | 28 | event = new ServerDataEvent(server, socket, DATA); 29 | } 30 | 31 | @Test 32 | public void getServer_None_ReturnsServer() { 33 | assertEquals(server, event.getNetworkInterface()); 34 | } 35 | 36 | @Test 37 | public void getSocket_None_ReturnsSocket() { 38 | assertEquals(socket, event.getSocket()); 39 | } 40 | 41 | @Test 42 | public void getData_None_ReturnsData() { 43 | assertArrayEquals(DATA, event.getData()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/comm/common/ServerDataEvent.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.common; 2 | 3 | import java.nio.channels.SocketChannel; 4 | 5 | import de.peteral.softplc.model.NetworkInterface; 6 | 7 | /** 8 | * Encapsulates a request from a client. 9 | * 10 | * @author peteral 11 | * 12 | */ 13 | public class ServerDataEvent { 14 | private final NetworkInterface server; 15 | private final SocketChannel socket; 16 | private final byte[] data; 17 | 18 | /** 19 | * Creates a new instance. 20 | * 21 | * @param server 22 | * server instance 23 | * @param socket 24 | * client socket to be used for sending a response 25 | * @param data 26 | * request data 27 | */ 28 | public ServerDataEvent(NetworkInterface server, SocketChannel socket, 29 | byte[] data) { 30 | this.server = server; 31 | this.socket = socket; 32 | this.data = data; 33 | } 34 | 35 | /** 36 | * @return the server 37 | */ 38 | public NetworkInterface getNetworkInterface() { 39 | return server; 40 | } 41 | 42 | /** 43 | * @return the socket 44 | */ 45 | public SocketChannel getSocket() { 46 | return socket; 47 | } 48 | 49 | /** 50 | * @return the data 51 | */ 52 | public byte[] getData() { 53 | return data; 54 | } 55 | } -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/memorytables/MemoryTableUpdateTask.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.memorytables; 2 | 3 | import java.util.Arrays; 4 | 5 | import de.peteral.softplc.model.Cpu; 6 | import de.peteral.softplc.model.MemoryTable; 7 | import de.peteral.softplc.protocol.CommunicationTask; 8 | 9 | /** 10 | * Virtual communication task used to update a memory table 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public class MemoryTableUpdateTask implements CommunicationTask { 16 | 17 | private final MemoryTable memoryTable; 18 | 19 | /** 20 | * Default constructor 21 | * 22 | * @param memoryTable 23 | */ 24 | public MemoryTableUpdateTask(MemoryTable memoryTable) { 25 | this.memoryTable = memoryTable; 26 | } 27 | 28 | @Override 29 | public void execute(Cpu cpu) { 30 | memoryTable.getVariables().forEach( 31 | variable -> { 32 | try { 33 | Object read = cpu.getMemory().read( 34 | variable.getVariable().get()); 35 | String strValue = (read.getClass().isArray()) ? Arrays 36 | .toString((Object[]) read) : "" + read; 37 | 38 | variable.getCurrentValue().set(strValue); 39 | } catch (Exception e) { 40 | variable.getCurrentValue().set(e.getMessage()); 41 | } 42 | }); 43 | } 44 | 45 | @Override 46 | public void onInvalidCpu(int slot) { 47 | // nothing to do here 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/memorytables/MemoryTableWriteTaskTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.memorytables; 2 | 3 | import static org.mockito.Mockito.verify; 4 | import static org.mockito.Mockito.when; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.Mock; 9 | import org.mockito.MockitoAnnotations; 10 | 11 | import de.peteral.softplc.model.Cpu; 12 | import de.peteral.softplc.model.Memory; 13 | import de.peteral.softplc.model.MemoryTableVariable; 14 | 15 | @SuppressWarnings("javadoc") 16 | public class MemoryTableWriteTaskTest { 17 | 18 | private static final String VALUE2 = "[1, 2, 3]"; 19 | private static final String VALUE1 = "10"; 20 | private static final String VAR2 = "var2"; 21 | private static final String VAR1 = "var1"; 22 | private MemoryTableWriteTask task; 23 | @Mock 24 | private Cpu cpu; 25 | @Mock 26 | private Memory memory; 27 | 28 | @Before 29 | public void setup() { 30 | MockitoAnnotations.initMocks(this); 31 | 32 | when(cpu.getMemory()).thenReturn(memory); 33 | 34 | task = new MemoryTableWriteTask(new MemoryTableVariable(VAR1, VALUE1), 35 | new MemoryTableVariable(VAR2, VALUE2)); 36 | } 37 | 38 | @Test 39 | public void execute_MockedCpu_WriteInvokedWithCorrectParametersOnMemory() { 40 | task.execute(cpu); 41 | 42 | verify(memory).parse(VAR1, VALUE1); 43 | verify(memory).parse(VAR2, VALUE2); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/DwordConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converter handling 32 bit unsigned. 8 | * 9 | * @author peteral 10 | */ 11 | public class DwordConverter implements Converter { 12 | 13 | @Override 14 | public Number[] createArray(int count) { 15 | return new Integer[count]; 16 | } 17 | 18 | @Override 19 | public void toBytes(Number value, ParsedAddress address, byte[] buffer, 20 | int offset) { 21 | buffer[offset + 3] = (byte) (value.longValue() & 0xFF); 22 | buffer[offset + 2] = (byte) ((value.longValue() & 0xFF00) >> 8); 23 | buffer[offset + 1] = (byte) ((value.longValue() & 0xFF0000) >> 16); 24 | buffer[offset] = (byte) ((value.longValue() & 0xFF000000) >> 24); 25 | } 26 | 27 | @Override 28 | public Number fromBytes(byte[] bytes, ParsedAddress address, int offset) { 29 | return (long) (DataTypeUtils.byteToInt(bytes[offset]) << 24) 30 | + (long) (DataTypeUtils.byteToInt(bytes[offset + 1]) << 16) 31 | + (DataTypeUtils.byteToInt(bytes[offset + 2]) << 8) 32 | + DataTypeUtils.byteToInt(bytes[offset + 3]); 33 | } 34 | 35 | @Override 36 | public void parseToBytes(String value, ParsedAddress address, 37 | byte[] buffer, int offset) { 38 | 39 | toBytes(Long.parseLong(value), address, buffer, offset); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/comm/common/ChangeRequest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.common; 2 | 3 | import java.nio.channels.SocketChannel; 4 | 5 | import de.peteral.softplc.comm.NetworkInterfaceImpl; 6 | 7 | /** 8 | * Change request for the {@link SocketChannel} of {@link NetworkInterfaceImpl}. 9 | * 10 | * @author peteral 11 | * 12 | */ 13 | public class ChangeRequest { 14 | @SuppressWarnings("javadoc") 15 | public static final int REGISTER = 1; 16 | @SuppressWarnings("javadoc") 17 | public static final int CHANGEOPS = 2; 18 | 19 | private final SocketChannel socket; 20 | private final int type; 21 | private final int ops; 22 | 23 | /** 24 | * Creates a new instance. 25 | * 26 | * @param socket 27 | * socket channel targeted by this change request. 28 | * @param type 29 | * change request type 30 | * @param ops 31 | * change request parameters 32 | */ 33 | public ChangeRequest(SocketChannel socket, int type, int ops) { 34 | this.socket = socket; 35 | this.type = type; 36 | this.ops = ops; 37 | } 38 | 39 | /** 40 | * @return the socket 41 | */ 42 | public SocketChannel getSocket() { 43 | return socket; 44 | } 45 | 46 | /** 47 | * @return the type 48 | */ 49 | public int getType() { 50 | return type; 51 | } 52 | 53 | /** 54 | * @return the ops 55 | */ 56 | public int getOps() { 57 | return ops; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/ScriptFile.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | 8 | import javafx.beans.property.SimpleStringProperty; 9 | import javafx.beans.property.StringProperty; 10 | 11 | /** 12 | * Encapsulates a JavaScript source file. 13 | * 14 | * @author peteral 15 | * 16 | */ 17 | public class ScriptFile { 18 | private final StringProperty fileName; 19 | private final StringProperty source; 20 | private final File file; 21 | 22 | /** 23 | * Creates a new instance. 24 | * 25 | * @param fileName 26 | * file name 27 | * @param file 28 | */ 29 | public ScriptFile(String fileName, File file) { 30 | this.file = file; 31 | this.source = new SimpleStringProperty(); 32 | this.fileName = new SimpleStringProperty(fileName); 33 | } 34 | 35 | /** 36 | * @return the source 37 | */ 38 | public StringProperty getSource() { 39 | return source; 40 | } 41 | 42 | /** 43 | * @return the fileName 44 | */ 45 | public StringProperty getFileName() { 46 | return fileName; 47 | } 48 | 49 | /** 50 | * Loads contents from disk. 51 | * 52 | * @throws IOException 53 | */ 54 | public void reload() throws IOException { 55 | byte[] bytes = Files.readAllBytes(Paths.get(file.getCanonicalPath())); 56 | source.set(new String(bytes)); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/classloader/FolderClassLoader.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.classloader; 2 | 3 | import java.io.File; 4 | import java.io.FilenameFilter; 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.net.URLClassLoader; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * This class loader covers all *.jar files in a folder. 13 | * 14 | * @author peteral 15 | * 16 | */ 17 | public class FolderClassLoader extends URLClassLoader { 18 | 19 | /** 20 | * Creates a new instance working with given folder. 21 | * 22 | * @param folder 23 | * all jars in this folder will be covered by this class loader 24 | */ 25 | public FolderClassLoader(File folder) { 26 | super(new URL[] {}); 27 | addJars(folder); 28 | } 29 | 30 | private void addJars(File folder) { 31 | if (!folder.exists()) { 32 | folder.mkdirs(); 33 | } 34 | 35 | String[] jarFiles = folder.list(new FilenameFilter() { 36 | 37 | @Override 38 | public boolean accept(File dir, String name) { 39 | return name.toLowerCase().endsWith(".jar"); 40 | } 41 | }); 42 | 43 | for (String jarFile : jarFiles) { 44 | try { 45 | File f = new File(folder, jarFile); 46 | super.addURL(f.toURI().toURL()); 47 | } catch (MalformedURLException e) { 48 | Logger.getLogger("FolderClassLoader").log(Level.WARNING, "Failed adding [" + jarFile + "]: ", e); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/comm/tasks/AbstractCommunicationTask.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.tasks; 2 | 3 | import java.nio.channels.SocketChannel; 4 | 5 | import de.peteral.softplc.model.Cpu; 6 | import de.peteral.softplc.model.NetworkInterface; 7 | import de.peteral.softplc.protocol.CommunicationTask; 8 | 9 | /** 10 | * Implements functionality common for all tasks. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public abstract class AbstractCommunicationTask implements CommunicationTask { 16 | 17 | private final NetworkInterface server; 18 | private final SocketChannel socket; 19 | private final CommunicationTaskFactory factory; 20 | 21 | /** 22 | * Creates a new instance. 23 | * 24 | * @param server 25 | * server instance 26 | * @param socket 27 | * socket to use for response 28 | * @param factory 29 | */ 30 | public AbstractCommunicationTask(NetworkInterface server, SocketChannel socket, 31 | CommunicationTaskFactory factory) { 32 | this.server = server; 33 | this.socket = socket; 34 | this.factory = factory; 35 | } 36 | 37 | @Override 38 | public void execute(Cpu cpu) { 39 | doExecute(cpu); 40 | 41 | sendResponse(); 42 | } 43 | 44 | /** 45 | * Sends response to the client. 46 | */ 47 | public void sendResponse() { 48 | byte[] response = factory.createResponse(this); 49 | server.send(socket, response); 50 | } 51 | 52 | protected abstract void doExecute(Cpu cpu); 53 | 54 | @Override 55 | public void onInvalidCpu(int slot) { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/PutGetServer.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.io.IOException; 4 | import java.nio.channels.SocketChannel; 5 | 6 | /** 7 | * Provides PUT/GET protocol interface for accessing the memory of this 8 | * {@link Plc}. 9 | *

10 | * Port - 102 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public interface PutGetServer { 16 | /** 17 | * Starts listening and processing PUT/GET requests on defined port. 18 | * 19 | * @param plc 20 | * handles this plc during execution 21 | * @throws IOException 22 | */ 23 | void start(Plc plc) throws IOException; 24 | 25 | /** 26 | * Stops listening on configured port. 27 | * 28 | * @throws IOException 29 | */ 30 | void stop() throws IOException; 31 | 32 | /** 33 | * Registers a new observer. 34 | * 35 | * @param o 36 | * observer instance 37 | */ 38 | void addObserver(PutGetServerObserver o); 39 | 40 | /** 41 | * Unregisters an observer. 42 | * 43 | * @param o 44 | * observer instance. 45 | */ 46 | void removeObserver(PutGetServerObserver o); 47 | 48 | /** 49 | * Notifies all registered observers about the corresponding event. 50 | * 51 | * @param event 52 | */ 53 | void notifyObservers(PutGetServerEvent event); 54 | 55 | /** 56 | * Sends data to a connected client socket. 57 | * 58 | * @param socket 59 | * client socket 60 | * @param data 61 | * data to be sent. 62 | */ 63 | void send(SocketChannel socket, byte[] data); 64 | } 65 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/NetworkInterface.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.io.IOException; 4 | import java.nio.channels.SocketChannel; 5 | 6 | /** 7 | * Provides PUT/GET protocol interface for accessing the memory of this 8 | * {@link Plc}. 9 | *

10 | * Port - 102 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public interface NetworkInterface { 16 | /** 17 | * Starts listening and processing PUT/GET requests on defined port. 18 | * 19 | * @param plc 20 | * handles this plc during execution 21 | * @throws IOException 22 | */ 23 | void start(Plc plc) throws IOException; 24 | 25 | /** 26 | * Stops listening on configured port. 27 | * 28 | * @throws IOException 29 | */ 30 | void stop() throws IOException; 31 | 32 | /** 33 | * Registers a new observer. 34 | * 35 | * @param o 36 | * observer instance 37 | */ 38 | void addObserver(PutGetServerObserver o); 39 | 40 | /** 41 | * Unregisters an observer. 42 | * 43 | * @param o 44 | * observer instance. 45 | */ 46 | void removeObserver(PutGetServerObserver o); 47 | 48 | /** 49 | * Notifies all registered observers about the corresponding event. 50 | * 51 | * @param event 52 | */ 53 | void notifyObservers(PutGetServerEvent event); 54 | 55 | /** 56 | * Sends data to a connected client socket. 57 | * 58 | * @param socket 59 | * client socket 60 | * @param data 61 | * data to be sent. 62 | */ 63 | void send(SocketChannel socket, byte[] data); 64 | } 65 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/Program.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import javafx.beans.property.LongProperty; 4 | import javafx.collections.ObservableList; 5 | 6 | /** 7 | * Represents the program which is cyclically executed by a {@link Cpu} 8 | * instance. 9 | *

10 | * Maintains it's own scripting context. 11 | * 12 | * @author peteral 13 | * 14 | */ 15 | public interface Program extends Runnable { 16 | /** 17 | * Compiles this program. 18 | *

19 | * Creates and initializes scripting context. 20 | * 21 | * @return true - success. 22 | */ 23 | boolean compile(); 24 | 25 | /** 26 | * Adds a new {@link ProgramCycleObserver} which s informed about the 27 | * program execution life cycle. 28 | * 29 | * @param observer 30 | */ 31 | void addObserver(ProgramCycleObserver observer); 32 | 33 | /** 34 | * Removes an existing {@link ProgramCycleObserver}. 35 | * 36 | * @param observer 37 | */ 38 | void removeObserver(ProgramCycleObserver observer); 39 | 40 | /** 41 | * 42 | * @return target cycle time in [ms] 43 | */ 44 | LongProperty getTargetCycleTime(); 45 | 46 | /** 47 | * 48 | * @return list of all source files for UI 49 | */ 50 | ObservableList getScriptFiles(); 51 | 52 | /** 53 | * 54 | * @return current cycle time 55 | */ 56 | LongProperty getCurrentCycleTime(); 57 | 58 | /** 59 | * Resets current cycle time counter. Invoked by CPU when the CPU is 60 | * stopped. 61 | */ 62 | void resetCycleTime(); 63 | 64 | /** 65 | * Reloads all source files from disk. 66 | */ 67 | void reloadFromDisk(); 68 | } 69 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/Plc.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.io.File; 4 | 5 | import javafx.collections.ObservableList; 6 | 7 | /** 8 | * Represents the virtual PLC system. 9 | *

10 | * Manages a set of {@link Cpu} instances organized by their slot number. 11 | *

12 | * Contains a PUT/GET Server which is started / stopped as the PLC starts / 13 | * stops. 14 | * 15 | * @author peteral 16 | */ 17 | public interface Plc { 18 | /** 19 | * Returns {@link Cpu} instance for given slot number. 20 | * 21 | * @param slot 22 | * slot number (index in the cpu array) 23 | * @return {@link Cpu} instance 24 | * @throws ArrayIndexOutOfBoundsException 25 | * for invalid slot numbers 26 | */ 27 | Cpu getCpu(int slot); 28 | 29 | /** 30 | * @return number of actually configured {@link Cpu} units 31 | */ 32 | int getCpuCount(); 33 | 34 | /** 35 | * Starts the PLC and all managed services. 36 | */ 37 | void start(); 38 | 39 | /** 40 | * Stops the PLC and all managed services. 41 | */ 42 | void stop(); 43 | 44 | /** 45 | * Checks whether the CPU with given slot is available. 46 | * 47 | * @param slot 48 | * slot number 49 | * @return true - CPU available 50 | */ 51 | boolean hasCpu(int slot); 52 | 53 | /** 54 | * @return all CPUs of this PLC as array 55 | */ 56 | ObservableList getCpus(); 57 | 58 | /** 59 | * 60 | * @return current configuration file path 61 | */ 62 | File getPath(); 63 | 64 | /** 65 | * sets current configuration file path 66 | * 67 | * @param path 68 | */ 69 | void setPath(File path); 70 | } 71 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/view/ApplicationControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.view; 2 | 3 | import static org.mockito.Mockito.verify; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.mockito.Mock; 9 | import org.mockito.MockitoAnnotations; 10 | 11 | import de.peteral.softplc.file.FileManager; 12 | import javafx.stage.Stage; 13 | 14 | @SuppressWarnings("javadoc") 15 | public class ApplicationControllerTest { 16 | 17 | @Rule 18 | public JavaFXThreadingRule jfxRule = new JavaFXThreadingRule(); 19 | private ApplicationController controller; 20 | @Mock 21 | private FileManager fileManager; 22 | @Mock 23 | private Stage stage; 24 | 25 | @Before 26 | public void setup() { 27 | MockitoAnnotations.initMocks(this); 28 | 29 | controller = new ApplicationController(); 30 | } 31 | 32 | @Test 33 | public void handleNew_MockedFileManager_InvokesCorrectMethod() { 34 | controller.setFileManager(fileManager); 35 | 36 | controller.handleNew(); 37 | 38 | verify(fileManager).newPlc(); 39 | } 40 | 41 | @Test 42 | public void handleAlwaysOnTop_OptionActive_InvokesCorrectStageMethod() { 43 | // TODO Java FX uses tons of final methods making mocking with mockito 44 | // impossible 45 | // need to use power-mock if I want to test this, same problem with Java 46 | // NIO API 47 | // fuk dis sheet 48 | 49 | // controller.setStage(stage); 50 | // controller.alwaysOnTopMenuItem = mock(CheckMenuItem.class); 51 | // when(controller.alwaysOnTopMenuItem.isSelected()).thenReturn(true); 52 | // 53 | // controller.handleAlwaysOnTop(); 54 | // 55 | // verify(stage).setAlwaysOnTop(true); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/comm/PutGetServerImplTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm; 2 | 3 | import static org.mockito.Mockito.*; 4 | 5 | import java.io.IOException; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | import de.peteral.softplc.model.Plc; 13 | import de.peteral.softplc.model.PutGetServerEvent; 14 | import de.peteral.softplc.model.PutGetServerObserver; 15 | 16 | @SuppressWarnings("javadoc") 17 | public class PutGetServerImplTest { 18 | 19 | @Mock 20 | private Plc plc; 21 | private NetworkInterfaceImpl server; 22 | @Mock 23 | private PutGetServerObserver observer; 24 | @Mock 25 | private PutGetServerEvent event; 26 | 27 | @Before 28 | public void setup() throws IOException { 29 | MockitoAnnotations.initMocks(this); 30 | 31 | server = new NetworkInterfaceImpl(); 32 | } 33 | 34 | @Test 35 | public void addObserver_AnObserver_ObserverInvokedWhenNotifyListenersIsCalled() { 36 | server.addObserver(observer); 37 | 38 | server.notifyObservers(event); 39 | 40 | verify(observer).onTelegram(event); 41 | } 42 | 43 | @Test 44 | public void addObserver_SameObserverTwice_ObserverInvokedOnceWhenNotifyListenersIsCalled() { 45 | server.addObserver(observer); 46 | server.addObserver(observer); 47 | 48 | server.notifyObservers(event); 49 | 50 | verify(observer).onTelegram(event); 51 | } 52 | 53 | @Test 54 | public void removeObserver_RegisteredObserver_ObserverNotInvokedWhenNotifyListenersIsCalled() { 55 | server.addObserver(observer); 56 | server.removeObserver(observer); 57 | 58 | server.notifyObservers(event); 59 | 60 | verify(observer, never()).onTelegram(event); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/Converter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | 5 | /** 6 | * Classes implementing this interface are responsible for converting of java 7 | * types to byte arrays and vice versa. 8 | *

9 | * Only converts a single value does not handle arrays. 10 | *

11 | * All number converters work with double as it is the only type used by 12 | * javascript. 13 | * 14 | * @param 15 | * 16 | * @author peteral 17 | */ 18 | public interface Converter { 19 | /** 20 | * Converts java type to byte array 21 | * 22 | * @param value 23 | * the java value to be converted 24 | * @param address 25 | * @param buffer 26 | * byte buffer 27 | * @param offset 28 | */ 29 | void toBytes(T value, ParsedAddress address, byte[] buffer, int offset); 30 | 31 | /** 32 | * Converts byte array to java data type. 33 | * 34 | * @param bytes 35 | * source byte array 36 | * @param address 37 | * @param offset 38 | * @return converted java value 39 | */ 40 | T fromBytes(byte[] bytes, ParsedAddress address, int offset); 41 | 42 | /** 43 | * Creates an array of java elements. 44 | * 45 | * @param count 46 | * requested size of the array. 47 | * @return uninitialized array of element 48 | */ 49 | T[] createArray(int count); 50 | 51 | /** 52 | * Converts string to byte array 53 | * 54 | * @param value 55 | * the java value to be converted 56 | * @param address 57 | * @param buffer 58 | * byte buffer 59 | * @param offset 60 | */ 61 | void parseToBytes(String value, ParsedAddress address, byte[] buffer, 62 | int offset); 63 | } 64 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/ErrorLogEntry.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.time.LocalDate; 4 | import java.util.logging.Level; 5 | 6 | import javafx.beans.property.ObjectProperty; 7 | import javafx.beans.property.SimpleObjectProperty; 8 | import javafx.beans.property.SimpleStringProperty; 9 | import javafx.beans.property.StringProperty; 10 | 11 | /** 12 | * Error log entry for display in UI. 13 | * 14 | * Last X logged entries are remembered. 15 | * 16 | * @author peteral 17 | * 18 | */ 19 | public class ErrorLogEntry { 20 | private final ObjectProperty timestamp = new SimpleObjectProperty<>(); 21 | private final StringProperty level = new SimpleStringProperty(); 22 | private final StringProperty module = new SimpleStringProperty(); 23 | private final StringProperty message = new SimpleStringProperty(); 24 | 25 | /** 26 | * Initializes new instance. 27 | * 28 | * @param level 29 | * @param module 30 | * @param message 31 | */ 32 | public ErrorLogEntry(Level level, String module, String message) { 33 | this.level.set(level.getName()); 34 | this.module.set(module); 35 | this.timestamp.set(LocalDate.now()); 36 | this.message.set(message); 37 | } 38 | 39 | /** 40 | * @return the timestamp 41 | */ 42 | public ObjectProperty getTimestamp() { 43 | return timestamp; 44 | } 45 | 46 | /** 47 | * @return the level 48 | */ 49 | public StringProperty getLevel() { 50 | return level; 51 | } 52 | 53 | /** 54 | * @return the module 55 | */ 56 | public StringProperty getModule() { 57 | return module; 58 | } 59 | 60 | /** 61 | * @return the message 62 | */ 63 | public StringProperty getMessage() { 64 | return message; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/memorytables/MemoryTableUpdateTaskTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.memorytables; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.mockito.Mockito.when; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.Mock; 9 | import org.mockito.MockitoAnnotations; 10 | 11 | import de.peteral.softplc.model.Cpu; 12 | import de.peteral.softplc.model.Memory; 13 | import de.peteral.softplc.model.MemoryTable; 14 | import de.peteral.softplc.model.MemoryTableVariable; 15 | 16 | @SuppressWarnings("javadoc") 17 | public class MemoryTableUpdateTaskTest { 18 | private static final Integer[] VALUE2 = new Integer[] { 1, 2, 3 }; 19 | private static final long VALUE1 = 10L; 20 | private static final String VAR2 = "var2"; 21 | private static final String VAR1 = "var1"; 22 | private MemoryTable memoryTable; 23 | private MemoryTableUpdateTask task; 24 | @Mock 25 | private Cpu cpu; 26 | @Mock 27 | private Memory memory; 28 | 29 | @Before 30 | public void setup() { 31 | MockitoAnnotations.initMocks(this); 32 | 33 | memoryTable = new MemoryTable(); 34 | memoryTable.getVariables().add(new MemoryTableVariable(VAR1, "")); 35 | memoryTable.getVariables().add(new MemoryTableVariable(VAR2, "")); 36 | 37 | when(cpu.getMemory()).thenReturn(memory); 38 | when(memory.read(VAR1)).thenReturn(VALUE1); 39 | when(memory.read(VAR2)).thenReturn(VALUE2); 40 | 41 | task = new MemoryTableUpdateTask(memoryTable); 42 | } 43 | 44 | @Test 45 | public void execute_ValidConfig_UpdatesVariablesInTable() { 46 | task.execute(cpu); 47 | 48 | assertEquals("10", memoryTable.getVariables().get(0).getCurrentValue() 49 | .get()); 50 | assertEquals("[1, 2, 3]", memoryTable.getVariables().get(1) 51 | .getCurrentValue().get()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /softplc/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | de.peteral.softplc 5 | softplc 6 | 1.2.8 7 | 8 | 9 | 1.8 10 | 1.8 11 | 12 | 13 | 14 | 15 | org.mockito 16 | mockito-all 17 | 1.9.5 18 | test 19 | 20 | 21 | junit 22 | junit 23 | 4.11 24 | test 25 | 26 | 27 | org.reflections 28 | reflections 29 | 0.9.9 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-jar-plugin 38 | 2.1 39 | 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | com.zenjava 49 | javafx-maven-plugin 50 | 2.0 51 | 52 | de.peteral.softplc.SoftplcApplication 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/datatype/StringConverter.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.datatype; 2 | 3 | import de.peteral.softplc.address.ParsedAddress; 4 | import de.peteral.softplc.model.Converter; 5 | 6 | /** 7 | * Converts string stored in an character array to / from a java String. 8 | *

9 | * 10 | * @author peteral 11 | * 12 | */ 13 | public class StringConverter implements Converter { 14 | 15 | @Override 16 | public String[] createArray(int count) { 17 | return new String[count]; 18 | } 19 | 20 | @Override 21 | public void toBytes(String value, ParsedAddress address, byte[] buffer, 22 | int offset) { 23 | assertValidBuffer(buffer, address, offset); 24 | 25 | byte[] bytes = value.getBytes(); 26 | for (int i = 0; i < value.length(); i++) { 27 | buffer[offset + i] = bytes[i]; 28 | } 29 | 30 | if (value.length() < address.getSize()) { 31 | buffer[offset + value.length()] = 0; 32 | } 33 | } 34 | 35 | private void assertValidBuffer(byte[] buffer, ParsedAddress address, 36 | int offset) { 37 | if (buffer.length < (address.getSize() + offset)) { 38 | throw new ConverterException("Invalid buffer length [" 39 | + buffer.length + "] for address [" + address + "]!"); 40 | } 41 | } 42 | 43 | @Override 44 | public String fromBytes(byte[] bytes, ParsedAddress address, int offset) { 45 | int length = Math.min(address.getSize(), bytes.length - offset); 46 | 47 | for (int i = offset; i < (offset + length); i++) { 48 | if (bytes[i] == 0) { 49 | length = (i - offset); 50 | } 51 | } 52 | 53 | return new String(bytes, offset, length); 54 | } 55 | 56 | @Override 57 | public void parseToBytes(String value, ParsedAddress address, 58 | byte[] buffer, int offset) { 59 | 60 | toBytes(value, address, buffer, offset); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/MemoryTableVariable.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import javafx.beans.property.SimpleStringProperty; 4 | import javafx.beans.property.StringProperty; 5 | 6 | /** 7 | * Represents one variable in a {@link MemoryTable}. 8 | * 9 | * @author peteral 10 | * 11 | */ 12 | public class MemoryTableVariable { 13 | private final StringProperty variable = new SimpleStringProperty(); 14 | private final StringProperty currentValue = new SimpleStringProperty(); 15 | private final StringProperty newValue = new SimpleStringProperty(); 16 | private final StringProperty comment = new SimpleStringProperty(); 17 | 18 | /** 19 | * This constructor is used during configuration parsing. 20 | * 21 | * @param name 22 | * variable name 23 | * @param newValue 24 | * new value 25 | */ 26 | public MemoryTableVariable(String name, String newValue, String comment) { 27 | variable.set(name); 28 | this.newValue.set(newValue); 29 | this.comment.set(comment); 30 | } 31 | 32 | /** 33 | * Default constructor. 34 | */ 35 | public MemoryTableVariable() { 36 | 37 | } 38 | 39 | /** 40 | * This constructor is used by memory table update tasks. 41 | * 42 | * @param name 43 | * @param newValue 44 | */ 45 | public MemoryTableVariable(String name, String newValue) { 46 | this(name, newValue, ""); 47 | } 48 | 49 | /** 50 | * @return the variable 51 | */ 52 | public StringProperty getVariable() { 53 | return variable; 54 | } 55 | 56 | /** 57 | * @return the currentValue 58 | */ 59 | public StringProperty getCurrentValue() { 60 | return currentValue; 61 | } 62 | 63 | /** 64 | * @return the newValue 65 | */ 66 | public StringProperty getNewValue() { 67 | return newValue; 68 | } 69 | 70 | /** 71 | * @return the comment 72 | */ 73 | public StringProperty getComment() { 74 | return comment; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/view/AddMemoryAreaRangeDialogController.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.view; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javafx.fxml.FXML; 7 | import javafx.scene.control.TextField; 8 | import javafx.stage.Stage; 9 | import de.peteral.softplc.memory.MemoryAreaImpl; 10 | import de.peteral.softplc.model.MemoryArea; 11 | 12 | /** 13 | * Controller class for the add memory range area dialog. 14 | * 15 | * @author peteral 16 | * 17 | */ 18 | public class AddMemoryAreaRangeDialogController { 19 | @FXML 20 | private TextField prefixField; 21 | @FXML 22 | private TextField rangeFromField; 23 | @FXML 24 | private TextField rangeToField; 25 | @FXML 26 | private TextField sizeField; 27 | private Stage dialogStage; 28 | private boolean okClicked; 29 | 30 | @FXML 31 | void handleOk() { 32 | // TODO add input validation 33 | okClicked = true; 34 | dialogStage.close(); 35 | } 36 | 37 | @FXML 38 | void handleCancel() { 39 | dialogStage.close(); 40 | } 41 | 42 | /** 43 | * assigns current stage 44 | * 45 | * @param dialogStage 46 | */ 47 | public void setDialogStage(Stage dialogStage) { 48 | this.dialogStage = dialogStage; 49 | } 50 | 51 | /** 52 | * 53 | * @return true - user confirmed dialog input 54 | */ 55 | public boolean isOkClicked() { 56 | return okClicked; 57 | } 58 | 59 | /** 60 | * Creates memory areas based on user input 61 | * 62 | * @return list of created memory areas 63 | */ 64 | public List createMemoryAreas() { 65 | List result = new ArrayList<>(); 66 | 67 | String prefix = prefixField.getText(); 68 | int from = Integer.parseInt(rangeFromField.getText()); 69 | int to = Integer.parseInt(rangeToField.getText()); 70 | int size = Integer.parseInt(sizeField.getText()); 71 | 72 | for (int i = from; i <= to; i++) { 73 | result.add(new MemoryAreaImpl(prefix + i, size, false)); 74 | } 75 | 76 | return result; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/MemorySnapshot.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.io.File; 4 | 5 | import de.peteral.softplc.file.FileUtil; 6 | import javafx.beans.property.BooleanProperty; 7 | import javafx.beans.property.SimpleBooleanProperty; 8 | import javafx.beans.property.SimpleStringProperty; 9 | import javafx.beans.property.StringProperty; 10 | 11 | /** 12 | * Represents a memory snapshot in the data model. 13 | *

14 | * Provides means to load memory snapshot to memory and save it from memory to a 15 | * file. 16 | * 17 | * @author peteral 18 | * 19 | */ 20 | public class MemorySnapshot { 21 | 22 | private final BooleanProperty isDefault = new SimpleBooleanProperty(); 23 | private final StringProperty fileName = new SimpleStringProperty(); 24 | 25 | /** 26 | * Creates a new instance. 27 | * 28 | * @param isDefault 29 | * - should this snapshot be loaded on application startup? 30 | * @param file 31 | * file name - absolute paths and paths relative to the 32 | * configuration file supported. 33 | */ 34 | public MemorySnapshot(boolean isDefault, String file) { 35 | this.isDefault.set(isDefault); 36 | this.fileName.set(file); 37 | } 38 | 39 | /** 40 | * 41 | * @return default memory snapshot is loaded during application startup 42 | */ 43 | public BooleanProperty isDefault() { 44 | return isDefault; 45 | } 46 | 47 | /** 48 | * 49 | * @return name of the file containing the memory snapshot 50 | */ 51 | public StringProperty getFileName() { 52 | return fileName; 53 | } 54 | 55 | /** 56 | * Returns addressed file as File 57 | * 58 | * @param baseFile 59 | * base file for relative paths 60 | * @return File representation of the snapshot 61 | */ 62 | public File getFile(File baseFile) { 63 | String res = FileUtil.toAbsolute(getFileName().get(), baseFile); 64 | if (res == null) { 65 | res = getFileName().get(); 66 | } 67 | 68 | return new File(res); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/file/FileUtil.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.file; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | 7 | import de.peteral.softplc.view.error.ErrorDialog; 8 | 9 | /** 10 | * This utility class helps with conversion between relative and absolute file 11 | * paths. 12 | * 13 | * @author peteral 14 | * 15 | */ 16 | public final class FileUtil { 17 | 18 | private FileUtil() { 19 | } 20 | 21 | /** 22 | * Translates path relative to basePath to absolute path 23 | * 24 | * @param file 25 | * relative path 26 | * @param basePath 27 | * base path (relative path is relative to this file) 28 | * @return absolute path, null on error 29 | */ 30 | public static String toAbsolute(String file, File basePath) { 31 | File f = new File(file); 32 | if ((basePath != null) && !f.isAbsolute()) { 33 | try { 34 | Path pathBase = Paths.get(basePath.getCanonicalPath()); 35 | Path pathRelative = Paths.get(f.getPath()); 36 | Path pathAbsolute = pathBase.resolve(pathRelative); 37 | return pathAbsolute.toString(); 38 | } catch (Exception e) { 39 | ErrorDialog.show("Failed converting to absolute", e); 40 | } 41 | } 42 | 43 | return null; 44 | } 45 | 46 | /** 47 | * Translates absolute path to path relative a base path. 48 | * 49 | * @param file 50 | * absolute file path 51 | * @param basePath 52 | * base path 53 | * @return path relative to base path, null on error 54 | */ 55 | public static String toRelative(String file, File basePath) { 56 | File f = new File(file); 57 | if (f.isAbsolute() && (basePath != null)) { 58 | try { 59 | Path pathAbsolute = Paths.get(f.getCanonicalPath()); 60 | Path pathBase = Paths.get(basePath.getCanonicalPath()); 61 | Path pathRelative = pathBase.relativize(pathAbsolute); 62 | 63 | return pathRelative.toString(); 64 | } catch (Exception e) { 65 | ErrorDialog.show("Failed converting to relative", e); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/comm/common/ClientChannelCacheTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm.common; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertNull; 6 | import static org.junit.Assert.assertSame; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.io.IOException; 10 | import java.net.SocketAddress; 11 | import java.nio.channels.SocketChannel; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.mockito.Mock; 16 | import org.mockito.MockitoAnnotations; 17 | 18 | @SuppressWarnings("javadoc") 19 | public class ClientChannelCacheTest { 20 | private static final int SLOT = 10; 21 | @Mock 22 | private SocketChannel socket; 23 | @Mock 24 | private SocketAddress address; 25 | 26 | @Before 27 | public void setup() throws IOException { 28 | MockitoAnnotations.initMocks(this); 29 | 30 | when(socket.getRemoteAddress()).thenReturn(address); 31 | 32 | ClientChannelCache.getInstance().clear(); 33 | } 34 | 35 | @Test 36 | public void getInstance_None_ReturnsCacheInstance() { 37 | assertNotNull(ClientChannelCache.getInstance()); 38 | } 39 | 40 | @Test 41 | public void getInstance_InvokedTwice_ReturnsSameInstance() { 42 | assertSame(ClientChannelCache.getInstance(), 43 | ClientChannelCache.getInstance()); 44 | } 45 | 46 | @Test 47 | public void addChannel_AnyChannel_GetSlotReturnsCorrectSlot() { 48 | ClientChannelCache.getInstance().addChannel(socket, SLOT); 49 | 50 | assertEquals(SLOT, 51 | (int) ClientChannelCache.getInstance().getSlot(socket)); 52 | } 53 | 54 | @Test 55 | public void getClientCount_OneChannel_ReturnsOne() { 56 | ClientChannelCache.getInstance().addChannel(socket, SLOT); 57 | 58 | assertEquals(1, 59 | ClientChannelCache.getInstance().getConnectionCount(SLOT)); 60 | } 61 | 62 | @Test 63 | public void remove_RegisteredSocket_SocketRemoved() { 64 | ClientChannelCache.getInstance().addChannel(socket, SLOT); 65 | ClientChannelCache.getInstance().removeChannel(socket); 66 | 67 | assertNull(ClientChannelCache.getInstance().getSlot(socket)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/model/MemoryArea.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.model; 2 | 3 | import java.util.logging.Logger; 4 | 5 | import javafx.beans.property.IntegerProperty; 6 | import javafx.beans.property.StringProperty; 7 | 8 | /** 9 | * Represents a memory area in the {@link Cpu}. 10 | *

11 | * Following memory areas are available: 12 | *

20 | * 21 | * @author peteral 22 | */ 23 | public interface MemoryArea { 24 | 25 | /** 26 | * @return memory area code 27 | */ 28 | StringProperty getAreaCode(); 29 | 30 | /** 31 | * Reads data from the memory. 32 | * 33 | * @param offset 34 | * start reading from this offset 35 | * @param length 36 | * number of bytes 37 | * @return byte array containing data from requested memory area 38 | * @throws MemoryAccessViolationException 39 | * for invalid parameters 40 | */ 41 | byte[] readBytes(int offset, int length); 42 | 43 | /** 44 | * Writes data to memory. 45 | * 46 | * @param offset 47 | * offset of the first byte to be written to 48 | * @param data 49 | * data to be written (length according the length of the data 50 | * array) 51 | * @throws MemoryAccessViolationException 52 | * for invalid parameters 53 | */ 54 | void writeBytes(int offset, byte[] data); 55 | 56 | /** 57 | * Sets a bit in memory. 58 | * 59 | * @param offset 60 | * byte offset 61 | * @param bitNumber 62 | * bit number 63 | * @param value 64 | * new value 65 | * @throws MemoryAccessViolationException 66 | * for invalid parameters 67 | */ 68 | void setBit(int offset, int bitNumber, boolean value); 69 | 70 | /** 71 | * @return total size of this memory area in bytes 72 | */ 73 | IntegerProperty getSize(); 74 | 75 | /** 76 | * @return logger for this memory area 77 | */ 78 | Logger getLogger(); 79 | 80 | /** 81 | * 82 | * @return default memory area is added automatically even if not configured 83 | */ 84 | boolean isDefaultArea(); 85 | 86 | /** 87 | * Resets memory area contents to zeros. 88 | */ 89 | void reset(); 90 | } 91 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/memory/MemoryAreaImplTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.memory; 2 | 3 | import static org.junit.Assert.assertArrayEquals; 4 | import static org.junit.Assert.assertEquals; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.MockitoAnnotations; 9 | 10 | import de.peteral.softplc.model.MemoryAccessViolationException; 11 | import de.peteral.softplc.model.MemoryArea; 12 | 13 | @SuppressWarnings("javadoc") 14 | public class MemoryAreaImplTest { 15 | 16 | private static final int SIZE = 10000; 17 | private static final String AREA_CODE = "M"; 18 | private MemoryArea area; 19 | 20 | @Before 21 | public void setup() { 22 | MockitoAnnotations.initMocks(this); 23 | 24 | area = new MemoryAreaImpl(AREA_CODE, SIZE, false); 25 | } 26 | 27 | @Test 28 | public void getAreaCode_None_ReturnsAreaCode() { 29 | assertEquals(AREA_CODE, area.getAreaCode().get()); 30 | } 31 | 32 | @Test 33 | public void writeBytes_ValidData_ReadBytesReturnsCorrectData() { 34 | byte[] data = { 0x01, 0x02, 0x03, 0x04 }; 35 | area.writeBytes(20, data); 36 | 37 | assertArrayEquals(data, area.readBytes(20, 4)); 38 | } 39 | 40 | @Test(expected = MemoryAccessViolationException.class) 41 | public void writeBytes_OffsetPlusLengthGreaterThanTotalSize_ThrowsException() { 42 | area.writeBytes(9999, new byte[] { 0x00, 0x00 }); 43 | } 44 | 45 | @Test(expected = MemoryAccessViolationException.class) 46 | public void writeBytes_OffsetGreaterTotalSize_ThrowsException() { 47 | area.writeBytes(10000, new byte[] { 0x00 }); 48 | } 49 | 50 | @Test(expected = MemoryAccessViolationException.class) 51 | public void writeBytes_OffsetNegative_ThrowsException() { 52 | area.writeBytes(-1, new byte[] { 0x00 }); 53 | } 54 | 55 | @Test 56 | public void setBit_ValidDataValueTrue_ReadBytesReturnsCorrectData() { 57 | area.writeBytes(0, new byte[] { 0, 0, 0, 0 }); 58 | 59 | area.setBit(2, 3, true); 60 | 61 | assertArrayEquals(new byte[] { 0x08 }, area.readBytes(2, 1)); 62 | } 63 | 64 | @Test 65 | public void setBit_ValidDataValueFalse_ReadBytesReturnsCorrectData() { 66 | area.writeBytes(0, new byte[] { 0, 0, 0x08, 0 }); 67 | 68 | area.setBit(2, 3, false); 69 | 70 | assertArrayEquals(new byte[] { 0x00 }, area.readBytes(2, 1)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /softplc/src/test/java/de/peteral/softplc/comm/RequestWorkerTest.java: -------------------------------------------------------------------------------- 1 | package de.peteral.softplc.comm; 2 | 3 | import static org.mockito.Mockito.*; 4 | 5 | import java.nio.channels.SocketChannel; 6 | 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import de.peteral.softplc.comm.common.ClientChannelCache; 14 | import de.peteral.softplc.comm.common.ServerDataEvent; 15 | import de.peteral.softplc.comm.tasks.CommunicationTaskFactory; 16 | import de.peteral.softplc.model.Cpu; 17 | import de.peteral.softplc.model.NetworkInterface; 18 | import de.peteral.softplc.model.Plc; 19 | import de.peteral.softplc.protocol.CommunicationTask; 20 | 21 | @SuppressWarnings("javadoc") 22 | public class RequestWorkerTest { 23 | 24 | private static final int CPU_SLOT = 10; 25 | @Mock 26 | private Plc plc; 27 | @Mock 28 | private CommunicationTaskFactory communicationTaskFactory; 29 | private RequestWorker worker; 30 | @Mock 31 | private NetworkInterface server; 32 | @Mock 33 | private SocketChannel socket; 34 | @Mock 35 | private ServerDataEvent event; 36 | @Mock 37 | private CommunicationTask task; 38 | @Mock 39 | private Cpu cpu; 40 | @Mock 41 | private ClientChannelCache cache; 42 | 43 | @Before 44 | public void setup() { 45 | MockitoAnnotations.initMocks(this); 46 | 47 | ClientChannelCache.installMock(cache); 48 | when(cache.getSlot(socket)).thenReturn(CPU_SLOT); 49 | 50 | when(event.getSocket()).thenReturn(socket); 51 | when(plc.getCpu(CPU_SLOT)).thenReturn(cpu); 52 | when(communicationTaskFactory.createTask(event)).thenReturn(task); 53 | when(event.getNetworkInterface()).thenReturn(server); 54 | when(plc.hasCpu(CPU_SLOT)).thenReturn(true); 55 | 56 | worker = new RequestWorker(plc, communicationTaskFactory); 57 | } 58 | 59 | @After 60 | public void teardown() { 61 | ClientChannelCache.installMock(null); 62 | } 63 | 64 | @Test(timeout = 2000) 65 | public void processData_WorkerRunning_CommunicationTaskCreatedAndDelegatedToCorrectCpu() 66 | throws InterruptedException { 67 | new Thread(worker).start(); 68 | 69 | worker.processData(event); 70 | 71 | Thread.sleep(250); 72 | 73 | worker.cancel(); 74 | 75 | verify(cpu).addCommunicationTask(task); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /softplc/src/main/java/de/peteral/softplc/view/AddMemoryAreaRangeDialog.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 33 |