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}")
}
}