Generated Code

Incomplete documentation

Page is not yet finished. Is incomplete and will contain inaccuracies.

There is a customizable code generator.

It uses Apache Freemarker as the template engine wrapped in a maven plugin with templates for Kotlin and Java included.

So if you have a schema

minimal.yaml
# $schema: https://modbus.basjes.nl/v1/ModbusSchema.json
description: 'A very simple demo schema'
schemaFeatureLevel: 1

blocks:
  - id: 'Block 1'
    description: 'The first block'
    fields:
      - id: 'Name'
        description: 'The name Field'
        # If a field NEVER changes value then set this to true
#        immutable: true
        # If a field is not a user level usable value set this to true (for example a scaling factor)
#        system: true
        expression: 'utf8(hr:0 # 12)'

tests:
  - id: 'Just to demo the test capability'
    input:
      - firstRegisterAddress: 'hr:0'
        registers: |2-
          # --------------------------------------
          # The name is here
          4e69 656c 7320 4261 736a 6573 0000 0000 0000 0000 
          0000 0000

    blocks:
      - id:          'Block 1'
        expected:
          'Name':    [ 'Niels Basjes' ]

You can generate native code during the build by putting this in the pom.xml:

pom.xml
<plugin>
    <groupId>nl.basjes.modbus</groupId>
    <artifactId>modbus-schema-maven-plugin</artifactId>
    <version>0.6.0</version>

    <configuration>
        <modbusSchemaFile>minimal.yaml</modbusSchemaFile>
        <packageName>nl.example</packageName>
        <className>Minimal</className>
        <language>kotlin</language>
    </configuration>

    <executions>
        <execution>
            <id>Generate Main Class</id>
            <goals>
                <goal>generate-main</goal>
            </goals>
        </execution>
        <execution>
            <id>Generate Test Class</id>
            <goals>
                <goal>generate-test</goal>
            </goals>
        </execution>
    </executions>
</plugin>

And then doing this

mvn generate-sources generate-test-sources 

will generate code like this:

/target/generated-sources/modbus-schema/kotlin/nl/example/Minimal.kt
//
// Generated using the nl.basjes.modbus:modbus-schema-maven-plugin:0.6.0
// Using the builtin template to generate Kotlin MAIN code.
// https://modbus.basjes.nl
//

// ===========================================================
//               !!! THIS IS GENERATED CODE !!!
// -----------------------------------------------------------
//       EVERY TIME THE SOFTWARE IS BUILD THIS FILE IS
//        REGENERATED AND ALL MANUAL CHANGES ARE LOST
// ===========================================================
package nl.example

import nl.basjes.modbus.device.api.ModbusDevice
import nl.basjes.modbus.device.exception.ModbusException
import nl.basjes.modbus.schema.Field
import nl.basjes.modbus.schema.Block
import nl.basjes.modbus.schema.SchemaDevice
import nl.basjes.modbus.schema.toSchemaDevice
import nl.basjes.modbus.schema.utils.StringTable

/**
 * A very simple demo schema
 */
open class Minimal {

    val schemaDevice = SchemaDevice()

    val tests = schemaDevice.tests

    fun connectBase(modbusDevice: ModbusDevice): Minimal {
        schemaDevice.connectBase(modbusDevice)
        return this
    }

    fun connect(modbusDevice: ModbusDevice): Minimal {
        schemaDevice.connect(modbusDevice)
        return this
    }

    /**
     * Update all registers related to the needed fields to be updated with a maximum age of the provided milliseconds
     * @param maxAge maximum age of the fields in milliseconds
     */
    @JvmOverloads
    fun update(maxAge: Long = 0) = schemaDevice.update(maxAge)

    /**
     * Update all registers related to the specified field
     * @param field the Field that must be updated
     */
    fun update(field: Field) = schemaDevice.update(field)

    /**
     * Make sure all registers mentioned in all known fields are retrieved.
     */
    @JvmOverloads
    fun updateAll(maxAge: Long = 0) = schemaDevice.updateAll(maxAge)

    /**
     * @param field The field that must be kept up-to-date
     */
    fun need(field: Field) = schemaDevice.need(field)

    /**
     * @param field The field that no longer needs to be kept up-to-date
     */
    fun unNeed(field: Field) = schemaDevice.unNeed(field)

    /**
     * We want all fields to be kept up-to-date
     */
    fun needAll()  = schemaDevice.needAll()

    /**
     * We no longer want all fields to be kept up-to-date
     */
    fun unNeedAll()  = schemaDevice.unNeedAll()

    abstract class DeviceField(val field: Field) {
        /**
         * Retrieve the value of this field using the currently available device data.
         */
        abstract val value: Any?
        /**
         * We want this field to be kept up-to-date
         */
        fun need() = field.need()
        /**
         * We no longer want this field to be kept up-to-date
         */
        fun unNeed() = field.unNeed()
        /**
         * The unit of the returns value
         */
        val unit =  field.unit
        /**
         * The description of the Field
         */
        val description = field.description
        override fun toString(): String = if (value == null) { "null" } else { value.toString() }
    }

    // ==========================================
    /**
     * The first block
     */
    val block1 = Block1(schemaDevice);

    class Block1(schemaDevice: SchemaDevice) {
        val block: Block;

        /**
         * Directly update all fields in this Block
         */
        fun update() = block.fields.forEach { it.update() }

        /**
         * All fields in this Block must be kept up-to-date
         */
        fun need() = block.fields.forEach { it.need() }

        /**
         * All fields in this Block no longer need to be kept up-to-date
         */
        fun unNeed() = block.fields.forEach { it.unNeed() }


        // ==========================================
        /**
         * The name Field
         */
        public val name: Name
        public class Name(block: Block): DeviceField (
            Field.builder()
                 .block(block)
                 .id("Name")
                 .description("The name Field")
                 .expression("utf8(hr:00000 # 12)")
                 .unit("")
                 .immutable(false)
                 .system(false)
                 .fetchGroup("<<Block 1 | Name>>")
                 .build()) {
            override val value get() = field.stringValue
        }

        init {
            this.block = Block.builder()
              .schemaDevice(schemaDevice)
              .id("Block 1")
              .description("The first block")
              .build()

            this.name = Name(block);
        }

        override fun toString(): String {
            val table = StringTable()
            table.withHeaders("Block", "Field", "Value");
            toStringTable(table)
            return table.toString()
        }

        internal fun toStringTable(table: StringTable) {
            table
                .addRow("Block 1", "Name", "" + name.value)
        }
    }

    override fun toString(): String {
        val table = StringTable();
        table.withHeaders("Block", "Field", "Value")
        block1  .toStringTable(table)
        return table.toString()
    }

    init {
        require(schemaDevice.initialize()) { "Unable to initialize schema device" }
    }

}

and tests that recreate all provided testcases as junit tests:

/target/generated-test-sources/modbus-schema/kotlin/nl/example/TestMinimal.kt
//
// Generated using the nl.basjes.modbus:modbus-schema-maven-plugin:0.6.0
// Using the builtin template to generate Kotlin TEST code.
// https://modbus.basjes.nl
//

// ===========================================================
//               !!! THIS IS GENERATED CODE !!!
// -----------------------------------------------------------
//       EVERY TIME THE SOFTWARE IS BUILD THIS FILE IS
//        REGENERATED AND ALL MANUAL CHANGES ARE LOST
// ===========================================================
package nl.example

import nl.basjes.modbus.device.api.Address
import nl.basjes.modbus.device.memory.MockedModbusDevice
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.Test

/**
 * Testing the Minimal class for: A very simple demo schema
 */
internal class TestMinimal {

    @Test
    fun ensureValidSchema() {
        val schemaDevice = Minimal().schemaDevice
        val results = schemaDevice.verifyProvidedTests()
        assertTrue(results.logResults(), "Unable to verify all tests defined in the schema definition" )
    }

    // ==========================================
    @Test
    // Just to demo the test capability ()
    fun verifyProvidedTest_JustToDemoTheTestCapability() {
        val modbusDevice = MockedModbusDevice.builder().build()
        val minimal = Minimal().connect(modbusDevice)
        modbusDevice.addRegisters(Address.of("hr:00000"), """
            4E69 656C 7320 4261 736A 6573 0000 0000 0000 0000
            0000 0000
            """.trimIndent());
        minimal.updateAll()
        assertEquals("Niels Basjes", minimal.block1.name.value)
    }
}

And because a lot of the null checks and returntype related things have been generated into the code this all makes using the schema a lot easier:

/src/main/kotlin/nl/example/MinimalDemo.kt
package nl.example

import nl.basjes.modbus.device.api.MODBUS_STANDARD_TCP_PORT
import nl.basjes.modbus.device.plc4j.ModbusDevicePlc4j

fun doMinimalExample() {
    // The hostname to connect to
    val modbusIp   = "modbus.iot.basjes.nl"
    val modbusPort = MODBUS_STANDARD_TCP_PORT
    val modbusUnit = 1

    print("Modbus: Connecting...")
    // Connect to the real Modbus device over TCP using the Apache PLC4J library
    ModbusDevicePlc4j("modbus-tcp:tcp://${modbusIp}:${modbusPort}?unit-identifier=${modbusUnit}")
        .use { modbusDevice ->
            println(" done")

            // Get the schema as the generated code class
            val device = Minimal()

            // Connect this Schema Device to the physical device (via Plc4J in this case))
            device.connect(modbusDevice)

            // No need to guess the names of the blocks and fields.
            device.block1.name.need()

            // Now we tell the system all fields must be made up-to-date and we consider values
            // less than 5 seconds old to be "new enough".
            device.update(5000) // << Here the modbus calls are done.

            // No need to search for the return type of the Field (strong typed generated code).
            println("Name: ${device.block1.name.value}")
        }
}