Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-ties-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@embr-jvm/core-designer': patch
---

Add an Embr splash screen for unlicensed gateways.
5 changes: 5 additions & 0 deletions .changeset/cute-chefs-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@embr-jvm/core-designer': patch
---

Add an `Embr Help` button to the designer's Help menu.
6 changes: 6 additions & 0 deletions .changeset/fast-bears-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@embr-jvm/core-designer': patch
---

Add a button to the Designer's status bar for unlicensed gateways. Clicking on this button will navigate the user to Embr's documentation.
- The button's tooltip displays the license status for each Embr module.
49 changes: 49 additions & 0 deletions .changeset/modern-cities-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'@embr-modules/charts': major
'@embr-modules/thermo': major
'@embr-modules/periscope': minor
'@embr-modules/snmp': minor
---

Introduce an optional licensing model.

## Embr Module Licensing
- To offer a more complete support model, Musson Industrial is introducing optional licensing for the Embr suite.
- Don't worry, these modules will always remain free to use and open source.


Here's what's changing:

**Standard Edition**
- Embr modules are free to use but will run in trial mode when unlicensed.
- **No functionality will be impacted by an expired trial.**
- You can remove the trial status by purchasing a low-cost license from our web store.

**Maker Edition**
- Embr modules are free to use with no trial mode.
Licenses can be purchased at the [Musson Industrial Web Store](https://mussonindustrial.com/store).

## Expected Questions

- **Q:** Is Embr still open source?
- **A:** Yes! The entire Embr suite is still MIT-licensed, and this will never change.


- **Q:** Can I continue to use Embr Charts/Periscope/Thermo/SNMP for free?
- **A:** Yes! The modules will report as being in trial mode, but no functionality will be impacted. You will still continue to receive updates.


- **Q:** Why should I purchase a license?
- **A:** A license removes the trial banner and supports continued development of the Embr suite, helping ensure these modules remain useful, reliable, and actively maintained.


- **Q:** What if I upgrade from 8.1 to 8.3?
- **A:** License keys are not tied to a specific Ignition version and will never expire.


- **Q:** Will you offer redundant gateway, integrator, or bulk purchase discounts?
- **A:** We aim to keep licensing simple, affordable, and automated, so there are currently no additional discounts.


- **Q:** I have questions regarding licenses, special setups, or long-term support guarantees.
- **A:** We're here for you! For setups that fall outside the standard automated licensing process, please reach out so we can provide guidance.
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.mussonindustrial.embr.common

import com.inductiveautomation.ignition.common.model.PlatformEdition

object Embr {
val CHARTS = EmbrModuleMeta("com.mussonindustrial.embr.charts", "embr-charts", "/embr/charts")

val PERISCOPE =
EmbrModuleMeta("com.mussonindustrial.embr.periscope", "embr-periscope", "/embr/periscope")
val EVENT_STREAM =
EmbrModuleMeta(
"com.mussonindustrial.embr.eventstream",
"embr-event-stream",
"/embr/event-stream",
)

val SNMP = EmbrModuleMeta("com.mussonindustrial.embr.snmp", "embr-snmp", "/embr/snmp")
val THERMO = EmbrModuleMeta("com.mussonindustrial.embr.thermo", "embr-thermo", "/embr/thermo")

const val DOCUMENTATION_URL = "https://docs.mussonindustrial.com/"

fun isLicenseRequired(): Boolean {
return !PlatformEdition.isMaker()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package com.mussonindustrial.embr.designer

import com.inductiveautomation.ignition.common.licensing.LicenseMode
import com.inductiveautomation.ignition.common.modules.ModuleInfo
import com.inductiveautomation.ignition.designer.model.DesignerContext
import com.mussonindustrial.embr.common.EmbrCommonContextExtension

interface EmbrDesignerContext :
DesignerContext, EmbrCommonContextExtension, EmbrDesignerContextExtension
DesignerContext, EmbrCommonContextExtension, EmbrDesignerContextExtension {

val embrModules: List<ModuleInfo>
get() = modules.filter { it.id.contains("com.mussonindustrial.embr") }.sortedBy { it.name }

val unlicensedEmbrModules: List<ModuleInfo>
get() = embrModules.filter { getLicenseState(it.id).licenseMode != LicenseMode.Activated }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,50 @@ package com.mussonindustrial.embr.designer
import com.inductiveautomation.ignition.designer.model.DesignerContext
import com.mussonindustrial.embr.common.EmbrCommonContextExtension
import com.mussonindustrial.embr.common.EmbrCommonContextExtensionImpl
import com.mussonindustrial.embr.designer.gui.EmbrHelpMenuItem
import com.mussonindustrial.embr.designer.gui.EmbrStartupModal
import com.mussonindustrial.embr.designer.gui.EmbrStatusBarButton
import com.mussonindustrial.embr.designer.gui.onWindowOpenedOnce
import javax.swing.JFrame
import javax.swing.JMenu

open class EmbrDesignerContextImpl(private val context: DesignerContext) :
EmbrDesignerContext,
DesignerContext by context,
EmbrCommonContextExtension by EmbrCommonContextExtensionImpl(context)
EmbrCommonContextExtension by EmbrCommonContextExtensionImpl(context) {

init {
if (!isInitialized()) initialize()
}

val helpMenu: JMenu?
get() {
val frame = context.frame as JFrame
val jMenuBar = frame.jMenuBar
for (menuIndex in 0..jMenuBar.menuCount) {
val menu = jMenuBar.getMenu(menuIndex)
if (menu.text == "Help") return menu
}
return null
}

private fun initialize() {
setInitialized()

context.frame.onWindowOpenedOnce {
helpMenu?.insert(EmbrHelpMenuItem(), 1)
if (unlicensedEmbrModules.isNotEmpty()) {
EmbrStartupModal()
statusBar.addDisplay(EmbrStatusBarButton(this), 1)
}
}
}

private fun isInitialized(): Boolean {
return System.getProperty("embr.designer.initialized")?.isNotEmpty() == true
}

private fun setInitialized() {
System.setProperty("embr.designer.initialized", "true")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.mussonindustrial.embr.designer.gui

import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.swing.SwingUtilities

fun ScheduledExecutorService.animate(
durationMs: Long,
periodMs: Long = 16L,
update: (Float) -> Unit,
done: () -> Unit,
) {
val start = System.nanoTime()

val task =
object : Runnable {
lateinit var future: ScheduledFuture<*>

override fun run() {
val elapsed = (System.nanoTime() - start) / 1_000_000f
val t = (elapsed / durationMs).coerceIn(0f, 1f)

SwingUtilities.invokeLater {
update(t)
if (t >= 1f) {
future.cancel(false)
done()
}
}
}
}

task.future = scheduleAtFixedRate(task, 0, periodMs, TimeUnit.MILLISECONDS)
}

fun easeOut(t: Float) = 1 - (1 - t) * (1 - t)

fun easeIn(t: Float) = t * t
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mussonindustrial.embr.designer.gui

import com.inductiveautomation.ignition.client.icons.SvgIconUtil
import javax.swing.Icon

object EmbrDesignerIcons {
val emblem: Icon = SvgIconUtil.getIcon("mussonindustrial_emblem")
val help: Icon = SvgIconUtil.getIcon("mussonindustrial_help")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mussonindustrial.embr.designer.gui

import com.inductiveautomation.ignition.client.util.BrowserLauncher
import com.mussonindustrial.embr.common.Embr
import javax.swing.JMenuItem

class EmbrHelpMenuItem : JMenuItem("Embr Help") {
init {
toolTipText = "Launch the Embr user manual in a web browser"
icon = EmbrDesignerIcons.help
addActionListener { BrowserLauncher.openURL(Embr.DOCUMENTATION_URL) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mussonindustrial.embr.designer.gui

import java.awt.*
import java.util.concurrent.Executors
import javax.imageio.ImageIO
import javax.swing.*
import javax.swing.Timer

class EmbrStartupModal : JWindow() {

private val executor = Executors.newSingleThreadScheduledExecutor()
private val autocloseDelay = 1000
private val fadeDuration = 300L

init {
isAlwaysOnTop = true
background = Color(0, 0, 0, 0)
opacity = 0f

val icon = loadResourceIcon("/images/startup/modal.png")
val label = JLabel(icon).apply { isOpaque = false }

contentPane.add(label)
pack()
setLocationRelativeTo(null)

isVisible = true
fadeIn {
Timer(autocloseDelay) { fadeOut() }
.apply {
isRepeats = false
start()
}
}
}

private fun fadeIn(onComplete: () -> Unit) {
executor.animate(fadeDuration, update = { t -> opacity = easeIn(t) }, done = onComplete)
}

private fun fadeOut() {
executor.animate(
fadeDuration,
update = { t -> opacity = 1f - easeIn(t) },
done = {
dispose()
executor.shutdownNow()
},
)
}

private fun loadResourceIcon(path: String): ImageIcon? {
return try {
val res = this::class.java.getResource(path)
if (res != null) ImageIcon(ImageIO.read(res)) else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.mussonindustrial.embr.designer.gui

import com.inductiveautomation.ignition.client.util.BrowserLauncher
import com.inductiveautomation.ignition.common.licensing.LicenseMode
import com.mussonindustrial.embr.common.Embr
import com.mussonindustrial.embr.designer.EmbrDesignerContext
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JLabel
import javax.swing.SwingUtilities

class EmbrStatusBarButton(private val context: EmbrDesignerContext) : JLabel() {

val colorRed = "#ff5c5c"
val colorGreen = "#5cff7c"

init {
icon = EmbrDesignerIcons.emblem
text = if (context.unlicensedEmbrModules.isNotEmpty()) "Embr (UNLICENSED)" else ""
toolTipText = buildTooltipHtml()

addMouseListener(
object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (SwingUtilities.isLeftMouseButton(e)) {
BrowserLauncher.openURL(Embr.DOCUMENTATION_URL)
}
}
}
)
}

private fun buildTooltipHtml(): String {
val moduleList =
context.embrModules.joinToString("<br>") { module ->
val licenseMode = context.getLicenseState(module.id).licenseMode
val isLicensed =
licenseMode == LicenseMode.Activated || licenseMode == LicenseMode.Free
if (isLicensed) {
"<font color='$colorGreen'>${module.name}</font>"
} else {
"<font color='$colorRed'>${module.name} <b>(<u>UNLICENSED</u>)</b></font>"
}
}

return """
<html>
<b>Embr by Musson Industrial</b>
<p>This gateway is utilizing Embr, a collection of open-source modules by Musson Industrial.</p>
$moduleList
<br>
You can view the documentation by clicking here.<br>
Need support? We can help!
</html>
"""
.trimIndent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mussonindustrial.embr.designer.gui

import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent

fun Window.onWindowOpenedOnce(block: (WindowEvent) -> Unit) {
addWindowListener(
object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) {
removeWindowListener(this)
block(e)
}
}
)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading