Skip to content

Commit

Permalink
first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
benmusson committed Jan 6, 2025
1 parent 8bcb7d8 commit 96fd4c4
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.mussonindustrial.embr.perspective.common.component

import com.inductiveautomation.ignition.common.jsonschema.JsonSchema
import com.inductiveautomation.perspective.common.api.ComponentEventDescriptor

class ComponentSchemaLoader(
private val clazz: Class<*>,
private val componentId: String,
) {
fun getSchema(): JsonSchema {
return getJsonSchema("/schemas/components/${componentId}/props.json")
}

fun getEventDescriptor(name: String, description: String): ComponentEventDescriptor {
return ComponentEventDescriptor(
name,
description,
getJsonSchema("/schemas/components/${componentId}/events/$name.props.json")
)
}

fun getPaletteEntry(variantId: String, label: String, tooltip: String): PaletteEntry {
return PaletteEntry(
ComponentSchemaLoader::class.java,
componentId,
variantId,
label,
tooltip
)
}

private fun getJsonSchema(path: String): JsonSchema {
return JsonSchema.parse(ComponentSchemaLoader::class.java.getResourceAsStream(path))
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,28 @@
package com.mussonindustrial.ignition.embr.charts.component.chart

import com.inductiveautomation.ignition.common.jsonschema.JsonSchema
import com.inductiveautomation.perspective.common.api.ComponentDescriptor
import com.inductiveautomation.perspective.common.api.ComponentDescriptorImpl
import com.inductiveautomation.perspective.common.api.ComponentEventDescriptor
import com.mussonindustrial.embr.perspective.common.component.PaletteEntry
import com.mussonindustrial.embr.perspective.common.component.ComponentSchemaLoader
import com.mussonindustrial.embr.perspective.common.component.addPaletteEntry
import com.mussonindustrial.ignition.embr.charts.Components
import com.mussonindustrial.ignition.embr.charts.Meta.MODULE_ID

class SmoothieChart {
companion object {
var COMPONENT_ID: String = "embr.chart.smoothie-chart"
var SCHEMA: JsonSchema =
JsonSchema.parse(
Components::class
.java
.getResourceAsStream("/schemas/components/${COMPONENT_ID}/props.json")
)

var EVENTS =
private val schemaLoader = ComponentSchemaLoader(Components::class.java, COMPONENT_ID)
private val SCHEMA = schemaLoader.getSchema()
private val EVENTS =
listOf(
ComponentEventDescriptor(
"getChartData",
"Testing",
JsonSchema.parse(
Components::class
.java
.getResourceAsStream(
"/schemas/components/${COMPONENT_ID}/events/getChartData.json"
)
)
schemaLoader.getEventDescriptor(
"onChartUpdate",
"Called on an interval configured in the component's properties."
)
)

private var VARIANT_BASE =
PaletteEntry(
this::class.java,
COMPONENT_ID,
schemaLoader.getPaletteEntry(
"base",
"SmoothieChart",
"Smoothie Charts is a simple library for displaying smooth live time lines. "
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "object",
"properties": {
"chart": {
"type": "object",
"description": "Chart JavaScript Proxy Object",
"properties": {
"runAsync": {
"type": "object",
"description": "Run JavaScript code asynchronously."
},
"runBlocking": {
"type": "object",
"description": "Run blocking JavaScript code."
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
package com.mussonindustrial.ignition.embr.charts.component.chart

import com.inductiveautomation.ignition.common.TypeUtilities
import com.inductiveautomation.ignition.common.gson.JsonObject
import com.inductiveautomation.ignition.common.util.LogUtil
import com.inductiveautomation.perspective.common.api.PropertyType
import com.inductiveautomation.perspective.common.config.EventConfig
import com.inductiveautomation.perspective.common.property.Origin
import com.inductiveautomation.perspective.gateway.api.Component
import com.inductiveautomation.perspective.gateway.api.ComponentModelDelegate
import com.inductiveautomation.perspective.gateway.api.ScriptCallable
import com.inductiveautomation.perspective.gateway.binding.BindingUtils.toJsonDeep
import com.inductiveautomation.perspective.gateway.messages.EventFiredMsg
import com.inductiveautomation.perspective.gateway.property.PropertyTree
import com.inductiveautomation.perspective.gateway.property.PropertyTree.Subscription
import com.mussonindustrial.embr.perspective.gateway.component.ComponentDelegateJavaScriptProxy
import com.mussonindustrial.ignition.embr.charts.ChartsGatewayContext
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import org.python.core.PySequence

class SmoothieChartModelDelegate(component: Component) : ComponentModelDelegate(component) {

private val log = LogUtil.getModuleLogger("embr-charts", "SmoothieChartModelDelegate")
private val context = ChartsGatewayContext.instance.perspectiveContext
private val queue = component.session.queue()
private val props = PropsHandler(component.getPropertyTreeOf(PropertyType.props)!!)
private val proxies = hashMapOf<String, ComponentDelegateJavaScriptProxy>()
private var updater: ScheduledFuture<*>? = null

private val UPDATE_DATA_MESSAGE = "update-data"
private var scheduledUpdate: ScheduledFuture<*>? = null
private val intervalListener: Subscription = createIntervalListener()

override fun onStartup() {
component.mdc { log.debugf("Startup") }
updater =
context.scheduler.scheduleWithFixedDelay(
{ getChartData() },
0,
props.delayMillis,
TimeUnit.MILLISECONDS
)
scheduleUpdate()
}

override fun onShutdown() {
component.mdc { log.debugf("Shutdown") }
updater?.cancel(false)
}

fun sendData(values: PySequence) {
queue.submit {
val json = JsonObject().apply { add("values", TypeUtilities.pyToGson(values)) }
this.fireEvent(UPDATE_DATA_MESSAGE, json)
}
scheduledUpdate?.cancel(false)
intervalListener.unsubscribe()
}

override fun handleEvent(message: EventFiredMsg) {
Expand All @@ -59,15 +45,34 @@ class SmoothieChartModelDelegate(component: Component) : ComponentModelDelegate(
}
}

inner class ChartDataEvent {
@Suppress("unused")
fun sendData(values: PySequence) {
this@SmoothieChartModelDelegate.sendData(values)
private fun createIntervalListener(): Subscription {
return props.tree.subscribe("options.update.interval", Origin.allBut(Origin.Delegate)) {
scheduledUpdate?.cancel(false)

if (props.updateInterval <= 0) {
return@subscribe
}

scheduleUpdate()
}
}

private fun getChartData() {
component.fireEvent(EventConfig.COMPONENT_EVENTS, "getChartData", ChartDataEvent())
private fun scheduleUpdate() {
scheduledUpdate =
context.scheduler.scheduleWithFixedDelay(
{ onChartUpdate() },
0,
props.updateInterval,
TimeUnit.MILLISECONDS
)
}

inner class ChartDataEvent {
@Suppress("unused") val chart = getJavaScriptProxy("chart")
}

private fun onChartUpdate() {
component.fireEvent(EventConfig.COMPONENT_EVENTS, "onChartUpdate", ChartDataEvent())
}

@ScriptCallable
Expand All @@ -78,12 +83,12 @@ class SmoothieChartModelDelegate(component: Component) : ComponentModelDelegate(
}
}

inner class PropsHandler(private val tree: PropertyTree) {
val delayMillis: Long
inner class PropsHandler(val tree: PropertyTree) {
val updateInterval: Long
get() {
val viewPath = tree.read("options.delayMillis")
val viewPath = tree.read("options.update.interval")
if (viewPath.isEmpty) {
return 0
return 1000
}

return toJsonDeep(viewPath.get()).asLong
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,7 @@ import {
ComponentDelegateJavaScriptProxy,
JavaScriptRunEvent,
} from '@embr-js/perspective-client'
import React, {
MutableRefObject,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import {
IChartOptions,
ITimeSeriesOptions,
Expand All @@ -38,16 +31,19 @@ type SmoothieChartProps = {
series: SeriesProps[]
options: IChartOptions & {
delayMillis: number
update: {
interval: number
}
}
redraw: boolean
}

type SmoothieChartRef = {
appendData: (values: number[]) => void
canvasRef: RefObject<HTMLCanvasElement>
chartRef: MutableRefObject<SmoothieChart | null>
canvas: HTMLCanvasElement | null
chart: SmoothieChart | null
props: ComponentProps<SmoothieChartProps>
seriesRef: MutableRefObject<TimeSeries[]>
series: TimeSeries[]
}

function setOptions(chart: SmoothieChart, nextOptions: IChartOptions) {
Expand Down Expand Up @@ -127,13 +123,14 @@ export function SmoothieChartComponent(
/* Update Delegate Interface */
useEffect(() => {
if (props.store.delegate) {
console.log('creating delegate interface')
const delegate = props.store.delegate as SmoothieChartComponentDelegate
delegate.setInterface({
appendData,
canvasRef,
chartRef,
canvas: canvasRef.current,
chart: chartRef.current,
props,
seriesRef,
series: seriesRef.current,
})
}
}, [appendData, canvasRef, chartRef, seriesRef])
Expand Down Expand Up @@ -205,57 +202,21 @@ export function SmoothieChartComponent(
}

class SmoothieChartComponentDelegate extends ComponentStoreDelegate {
private chart?: SmoothieChartRef = undefined

private proxyProps: JsObject = {}
private jsProxy = new ComponentDelegateJavaScriptProxy(this, this.proxyProps)

private previousValues: number[] = []
private timeoutId: NodeJS.Timeout | undefined

constructor(component: AbstractUIElementStore) {
super(component)
this.updateStaleValues()
}

setInterface(chart: SmoothieChartRef) {
this.chart = chart
this.proxyProps.chart = chart
}

handleEvent(eventName: string, eventObject: JsObject): void {
if (this.jsProxy.handles(eventName)) {
this.jsProxy.handleEvent(eventObject as JavaScriptRunEvent)
}

if (eventName == 'data-update') {
const values = eventObject['values'] as number[]
this.notifyValues(values)
}
}

getStaleDelay(): number {
if (this.chart == undefined) {
return 100
} else {
return this.chart.props.props.options.delayMillis
}
}

updateStaleValues() {
console.log('updating stale values')
this.chart?.appendData(this.previousValues)
this.timeoutId = setTimeout(() => {
this.updateStaleValues()
}, this.getStaleDelay())
}

notifyValues(values: number[]) {
clearTimeout(this.timeoutId)
this.chart?.appendData(values)
this.previousValues = values
this.timeoutId = setTimeout(() => {
this.updateStaleValues()
}, this.getStaleDelay())
}
}

Expand All @@ -278,7 +239,7 @@ export const SmoothieChartComponentMeta: ComponentMeta = {
return {
series: tree.read('series', []),
options: tree.read('options', {}),
redraw: tree.read('redraw', undefined),
redraw: tree.read('redraw', false),
} as never
},
getViewComponent: function (): PComponent {
Expand Down

0 comments on commit 96fd4c4

Please sign in to comment.