diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index 9696415..064c27b 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -2,7 +2,7 @@ name: Publish new version upon tag commit on: push: tags: - - '*' + - '[0-9].[0-9].[0-9]' jobs: build: runs-on: ubuntu-latest @@ -29,24 +29,9 @@ jobs: --keystoreFile=keystore.pfx --keystorePassword="${{ secrets.KEYSTORE_PASSWORD }}" --certAlias=ignition-extensions - - name: Upload signed module - uses: actions/upload-artifact@v3 - with: - name: signed - path: build/Ignition-Extensions.modl - release: - runs-on: ubuntu-latest - needs: [build] - steps: - - uses: actions/checkout@v2 - - name: Download signed module - uses: actions/download-artifact@v3 - with: - name: signed - path: artifacts - name: Create release uses: marvinpinto/action-automatic-releases@latest with: repo_token: ${{ secrets.GITHUB_TOKEN }} prerelease: false - files: artifacts/* + files: build/Ignition-Extensions.modl diff --git a/README.md b/README.md index 12df72f..dbb67b9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,26 @@ # Ignition Extensions A (hopefully) community driven Ignition module project to house utilities that are often useful, but just too niche (or -have enough footguns) to go into Ignition itself. +potentially risky) to go into Ignition itself. # Usage -Simply download the .modl file from the [latest release](https://github.com/IgnitionModuleDevelopmentCommunity/ignition-extensions/releases) and install it to your gateway. +Simply download the .modl file from +the [latest release](https://github.com/IgnitionModuleDevelopmentCommunity/ignition-extensions/releases) and install it +to your gateway. + +# Contribution + +Contributions are welcome. This project is polyglot and set up for both Kotlin and Java. There are example utilities +written in both Kotlin and Java to extend from. Ideas for new features should start as issues for broader discussion. # Building This project uses Gradle, and the Gradle Module Plugin. Use `./gradlew build` to assemble artifacts, and `./gradlew zipModule` to build an unsigned module file for installation into a development gateway. -# Contribution +# Testing -Contributions are welcome. This project is polyglot and set up for both Kotlin and Java. There are example utilities -written in both Kotlin and Java to extend from. Ideas for new features should start as issues for broader discussion. +The easiest way to test is a local Docker installation. Simple run `docker compose up` in the root of this repository to +stand up a local development gateway. Use `./gradlew deployModl` to install the locally built module on that test +gateway. diff --git a/build.gradle.kts b/build.gradle.kts index 5f48818..f28130f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,3 +38,7 @@ ignitionModule { skipModlSigning.set(!findProperty("signModule").toString().toBoolean()) } + +tasks.deployModl { + hostGateway.set("http://localhost:18088") +} diff --git a/client/build.gradle.kts b/client/build.gradle.kts index faeb344..7577b92 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -9,6 +9,10 @@ java { } } +kotlin { + jvmToolchain(libs.versions.java.map(String::toInt).get()) +} + dependencies { compileOnly(libs.bundles.client) compileOnly(projects.common) diff --git a/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt b/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt index e1a1c12..24bc3f6 100644 --- a/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt +++ b/client/src/main/kotlin/org/imdc/extensions/client/ClientHook.kt @@ -4,9 +4,9 @@ import com.inductiveautomation.ignition.client.model.ClientContext import com.inductiveautomation.ignition.common.BundleUtil import com.inductiveautomation.ignition.common.licensing.LicenseState import com.inductiveautomation.ignition.common.script.ScriptManager -import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider import com.inductiveautomation.vision.api.client.AbstractClientModuleHook import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle @@ -20,11 +20,15 @@ class ClientHook : AbstractClientModuleHook() { BundleUtil.get().apply { addPropertyBundle() addPropertyBundle() + addPropertyBundle() } } override fun initializeScriptManager(manager: ScriptManager) { - manager.addScriptModule("system.dataset", DatasetExtensions, PropertiesFileDocProvider()) - manager.addScriptModule("system.util", UtilitiesExtensions(context), PropertiesFileDocProvider()) + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", ClientProjectExtensions(context), ExtensionDocProvider) + } } } diff --git a/client/src/main/kotlin/org/imdc/extensions/client/ClientProjectExtensions.kt b/client/src/main/kotlin/org/imdc/extensions/client/ClientProjectExtensions.kt new file mode 100644 index 0000000..828b638 --- /dev/null +++ b/client/src/main/kotlin/org/imdc/extensions/client/ClientProjectExtensions.kt @@ -0,0 +1,13 @@ +package org.imdc.extensions.client + +import com.inductiveautomation.ignition.client.model.ClientContext +import com.inductiveautomation.ignition.common.project.Project +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import org.imdc.extensions.common.ProjectExtensions + +class ClientProjectExtensions(private val context: ClientContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "ClientProjectExtensions") + override fun getProject(): Project { + return requireNotNull(context.project) + } +} diff --git a/client/src/main/resources/org/imdc/extensions/client/ClientProjectExtensions.properties b/client/src/main/resources/org/imdc/extensions/client/ClientProjectExtensions.properties new file mode 100644 index 0000000..776f5d2 --- /dev/null +++ b/client/src/main/resources/org/imdc/extensions/client/ClientProjectExtensions.properties @@ -0,0 +1,2 @@ +getProject.desc=Retrieves the current project. +getProject.returns=The current project as a RuntimeProject instance. diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 86ffa82..a925654 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -9,6 +9,10 @@ java { } } +kotlin { + jvmToolchain(libs.versions.java.map(String::toInt).get()) +} + dependencies { compileOnly(libs.ignition.common) testImplementation(libs.ignition.common) diff --git a/common/src/main/java/org/imdc/extensions/common/UtilitiesExtensions.java b/common/src/main/java/org/imdc/extensions/common/UtilitiesExtensions.java index 104c3c0..51d35cd 100644 --- a/common/src/main/java/org/imdc/extensions/common/UtilitiesExtensions.java +++ b/common/src/main/java/org/imdc/extensions/common/UtilitiesExtensions.java @@ -22,6 +22,7 @@ public UtilitiesExtensions(CommonContext context) { } @ScriptFunction(docBundlePrefix = "UtilitiesExtensions") + @UnsafeExtension public CommonContext getContext() { return context; } diff --git a/common/src/main/kotlin/org/imdc/extensions/common/ExtensionDocProvider.kt b/common/src/main/kotlin/org/imdc/extensions/common/ExtensionDocProvider.kt new file mode 100644 index 0000000..85bcb02 --- /dev/null +++ b/common/src/main/kotlin/org/imdc/extensions/common/ExtensionDocProvider.kt @@ -0,0 +1,34 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider +import com.inductiveautomation.ignition.common.script.hints.ScriptFunctionDocProvider +import java.lang.reflect.Method + +private val propertiesFileDocProvider = PropertiesFileDocProvider() + +private val WARNING = """ + THIS IS AN UNOFFICIAL IGNITION EXTENSION. + IT MAY RELY ON OR EXPOSE UNDOCUMENTED OR DANGEROUS FUNCTIONALITY. + USE AT YOUR OWN RISK. +""".trimIndent() + +object ExtensionDocProvider : ScriptFunctionDocProvider by propertiesFileDocProvider { + override fun getMethodDescription(path: String, method: Method): String { + val methodDescription: String? = propertiesFileDocProvider.getMethodDescription(path, method) + val unsafeAnnotation = method.getAnnotation() + + return buildString { + if (unsafeAnnotation != null) { + append("") + append(WARNING) + if (unsafeAnnotation.note.isNotEmpty()) { + append("
").append(unsafeAnnotation.note) + } + append("


") + } + append(methodDescription.orEmpty()) + } + } +} + +annotation class UnsafeExtension(val note: String = "") diff --git a/common/src/main/kotlin/org/imdc/extensions/common/ProjectExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/ProjectExtensions.kt new file mode 100644 index 0000000..d229648 --- /dev/null +++ b/common/src/main/kotlin/org/imdc/extensions/common/ProjectExtensions.kt @@ -0,0 +1,7 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.project.Project + +interface ProjectExtensions { + fun getProject(): Project +} diff --git a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt index d4bde98..8939632 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt @@ -4,19 +4,20 @@ import com.inductiveautomation.ignition.common.BundleUtil import com.inductiveautomation.ignition.common.Dataset import org.python.core.Py import org.python.core.PyObject +import java.lang.reflect.Method class PyObjectAppendable(target: PyObject) : Appendable { private val writeMethod = target.__getattr__("write") - override fun append(csq: CharSequence) = this.apply { + override fun append(csq: CharSequence): Appendable = this.apply { writeMethod.__call__(Py.newStringOrUnicode(csq.toString())) } - override fun append(csq: CharSequence, start: Int, end: Int) = this.apply { + override fun append(csq: CharSequence, start: Int, end: Int): Appendable = this.apply { append(csq.subSequence(start, end)) } - override fun append(c: Char) = this.apply { + override fun append(c: Char): Appendable = this.apply { append(c.toString()) } } @@ -48,3 +49,7 @@ inline fun BundleUtil.addPropertyBundle() { T::class.java.name.replace('.', '/'), ) } + +inline fun Method.getAnnotation(): T? { + return getAnnotation(T::class.java) +} diff --git a/common/src/main/resources/io/github/paulgriffith/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties similarity index 100% rename from common/src/main/resources/io/github/paulgriffith/extensions/common/DatasetExtensions.properties rename to common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties diff --git a/common/src/main/resources/io/github/paulgriffith/extensions/common/UtilitiesExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/UtilitiesExtensions.properties similarity index 100% rename from common/src/main/resources/io/github/paulgriffith/extensions/common/UtilitiesExtensions.properties rename to common/src/main/resources/org/imdc/extensions/common/UtilitiesExtensions.properties diff --git a/designer/build.gradle.kts b/designer/build.gradle.kts index 4ca7178..fab5670 100644 --- a/designer/build.gradle.kts +++ b/designer/build.gradle.kts @@ -9,6 +9,10 @@ java { } } +kotlin { + jvmToolchain(libs.versions.java.map(String::toInt).get()) +} + dependencies { compileOnly(libs.bundles.designer) compileOnly(projects.common) diff --git a/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt index 52a0c2e..a4e58e1 100644 --- a/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt +++ b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerHook.kt @@ -3,10 +3,10 @@ package org.imdc.extensions.designer import com.inductiveautomation.ignition.common.BundleUtil import com.inductiveautomation.ignition.common.licensing.LicenseState import com.inductiveautomation.ignition.common.script.ScriptManager -import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHook import com.inductiveautomation.ignition.designer.model.DesignerContext import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle @@ -20,11 +20,15 @@ class DesignerHook : AbstractDesignerModuleHook() { BundleUtil.get().apply { addPropertyBundle() addPropertyBundle() + addPropertyBundle() } } override fun initializeScriptManager(manager: ScriptManager) { - manager.addScriptModule("system.dataset", DatasetExtensions, PropertiesFileDocProvider()) - manager.addScriptModule("system.util", UtilitiesExtensions(context), PropertiesFileDocProvider()) + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", DesignerProjectExtensions(context), ExtensionDocProvider) + } } } diff --git a/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerProjectExtensions.kt b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerProjectExtensions.kt new file mode 100644 index 0000000..df22708 --- /dev/null +++ b/designer/src/main/kotlin/org/imdc/extensions/designer/DesignerProjectExtensions.kt @@ -0,0 +1,38 @@ +package org.imdc.extensions.designer + +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.designer.IgnitionDesigner +import com.inductiveautomation.ignition.designer.model.DesignerContext +import com.inductiveautomation.ignition.designer.project.DesignableProject +import org.apache.commons.lang3.reflect.MethodUtils +import org.imdc.extensions.common.ProjectExtensions +import org.imdc.extensions.common.UnsafeExtension + +class DesignerProjectExtensions(private val context: DesignerContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + override fun getProject(): DesignableProject { + return requireNotNull(context.project) + } + + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + fun save() { + MethodUtils.invokeMethod( + context.frame, + true, // forceAccess + "handleSave", + false, // saveAs + null, // newName + false, // commitOnly + false, // skipReopen + false, // showDialog + ) + } + + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + fun update() { + (context.frame as IgnitionDesigner).updateProject() + } +} diff --git a/designer/src/main/resources/org/imdc/extensions/designer/DesignerProjectExtensions.properties b/designer/src/main/resources/org/imdc/extensions/designer/DesignerProjectExtensions.properties new file mode 100644 index 0000000..c5ac1cc --- /dev/null +++ b/designer/src/main/resources/org/imdc/extensions/designer/DesignerProjectExtensions.properties @@ -0,0 +1,6 @@ +getProject.desc=Retrieves the current project. +getProject.returns=The current project as a DesignableProject instance. +update.desc=Pulls in external changes made to this project from the gateway. +update.returns=None +save.desc=Programmatically invokes the save action. +save.returns=None diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..911d51b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + gateway: + image: inductiveautomation/ignition:8.1.20 + ports: + - 18088:8088 + environment: + GATEWAY_ADMIN_PASSWORD: password + IGNITION_EDITION: standard + ACCEPT_IGNITION_EULA: "Y" + volumes: + - gateway-data:/usr/local/bin/ignition/data + command: > + -n Ignition-module-dev + -d + -- + -Dignition.allowunsignedmodules=true + -Dia.developer.moduleupload=true + +volumes: + gateway-data: diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index d91e1ed..623b626 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -9,6 +9,10 @@ java { } } +kotlin { + jvmToolchain(libs.versions.java.map(String::toInt).get()) +} + dependencies { compileOnly(libs.bundles.gateway) compileOnly(projects.common) diff --git a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt index d5e3af1..b4bc735 100644 --- a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt +++ b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt @@ -3,10 +3,10 @@ package org.imdc.extensions.gateway import com.inductiveautomation.ignition.common.BundleUtil import com.inductiveautomation.ignition.common.licensing.LicenseState import com.inductiveautomation.ignition.common.script.ScriptManager -import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook import com.inductiveautomation.ignition.gateway.model.GatewayContext import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider import org.imdc.extensions.common.UtilitiesExtensions import org.imdc.extensions.common.addPropertyBundle @@ -20,6 +20,7 @@ class GatewayHook : AbstractGatewayModuleHook() { BundleUtil.get().apply { addPropertyBundle() addPropertyBundle() + addPropertyBundle() } } @@ -27,8 +28,11 @@ class GatewayHook : AbstractGatewayModuleHook() { override fun shutdown() {} override fun initializeScriptManager(manager: ScriptManager) { - manager.addScriptModule("system.dataset", DatasetExtensions, PropertiesFileDocProvider()) - manager.addScriptModule("system.util", UtilitiesExtensions(context), PropertiesFileDocProvider()) + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", GatewayProjectExtensions(context), ExtensionDocProvider) + } } override fun isFreeModule(): Boolean = true diff --git a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayProjectExtensions.kt b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayProjectExtensions.kt new file mode 100644 index 0000000..69950a2 --- /dev/null +++ b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayProjectExtensions.kt @@ -0,0 +1,24 @@ +package org.imdc.extensions.gateway + +import com.inductiveautomation.ignition.common.project.RuntimeProject +import com.inductiveautomation.ignition.common.script.ScriptContext +import com.inductiveautomation.ignition.common.script.hints.ScriptArg +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.gateway.model.GatewayContext +import org.imdc.extensions.common.ProjectExtensions +import org.python.core.Py + +class GatewayProjectExtensions(private val context: GatewayContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") + override fun getProject(): RuntimeProject { + val defaultProject = ScriptContext.defaultProject() ?: throw Py.EnvironmentError("No context project populated") + return requireNotNull(getProject(defaultProject)) { "No such project $defaultProject" } + } + + @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") + fun getProject( + @ScriptArg("project", optional = true) project: String, + ): RuntimeProject? { + return context.projectManager.getProject(project).orElse(null) + } +} diff --git a/gateway/src/main/resources/org/imdc/extensions/gateway/GatewayProjectExtensions.properties b/gateway/src/main/resources/org/imdc/extensions/gateway/GatewayProjectExtensions.properties new file mode 100644 index 0000000..4be8f31 --- /dev/null +++ b/gateway/src/main/resources/org/imdc/extensions/gateway/GatewayProjectExtensions.properties @@ -0,0 +1,3 @@ +getProject.desc=Retrieves a project by name, or None. +getProject.param.project=The name of the project to retrieve. Defaults to the current project, if known. +getProject.returns=The specified project as a RuntimeProject instance. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c0a292..8a820dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,9 @@ [versions] java = "11" -kotlin = "1.7.10" +kotlin = "1.7.20" kotest = "5.4.1" ignition = "8.1.0" -#coroutines = "1.6.3" -#serialization = "1.3.3" -#ktor = "2.0.3" - [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } #serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } @@ -48,4 +44,4 @@ kotest = [ "kotest-data", "kotest-junit", "kotest-property", -] \ No newline at end of file +]