diff --git a/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java b/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java new file mode 100644 index 0000000000..76b4628079 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java @@ -0,0 +1,22 @@ +package org.myrobotlab.framework.interfaces; + +import org.myrobotlab.framework.Message; + +public interface JsonInvoker { + + /** + * No parameter method + * @param method + * @return + */ + public Object invoke(String method); + + /** + * Encoded parameters as a JSON String (encoded once!) + * @param method + * @param encodedParameters + * @return + */ + public Object invoke(String method, String encodedParameters); + +} diff --git a/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java b/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java new file mode 100644 index 0000000000..ef2a7c0056 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java @@ -0,0 +1,12 @@ +package org.myrobotlab.framework.interfaces; + +public interface JsonSender { + + /** + * Send interface which takes a json encoded Message. + * For schema look at org.myrobotlab.framework.Message + * @param jsonEncodedMessage + */ + public void send(String jsonEncodedMessage); + +} diff --git a/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java b/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java index ba0f5f1f19..c05a7b16e2 100644 --- a/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java +++ b/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java @@ -3,7 +3,7 @@ import org.myrobotlab.framework.Message; import org.myrobotlab.framework.TimeoutException; -public interface MessageSender extends NameProvider { +public interface MessageSender extends NameProvider, SimpleMessageSender { /** * Send invoking messages to remote location to invoke {name} instance's diff --git a/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java b/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java new file mode 100644 index 0000000000..a4d6d54a64 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java @@ -0,0 +1,9 @@ +package org.myrobotlab.framework.interfaces; + +import org.myrobotlab.framework.Message; + +public interface SimpleMessageSender { + + public void send(Message msg); + +} diff --git a/src/main/java/org/myrobotlab/service/Gpt3.java b/src/main/java/org/myrobotlab/service/Gpt3.java index 19da929f49..67ed717639 100644 --- a/src/main/java/org/myrobotlab/service/Gpt3.java +++ b/src/main/java/org/myrobotlab/service/Gpt3.java @@ -119,10 +119,7 @@ public Response getResponse(String text) { @SuppressWarnings({ "unchecked", "rawtypes" }) Map textObject = (Map) choices.get(0); responseText = (String) textObject.get("text"); - if (responseText != null) { - // /completions - invoke("publishText", responseText); - } else { + if (responseText == null) { // /chat/completions @SuppressWarnings({ "unchecked", "rawtypes" }) Map content = (Map)textObject.get("message"); @@ -156,6 +153,7 @@ public Response getResponse(String text) { if (responseText != null && responseText.length() > 0) { invoke("publishUtterance", utterance); invoke("publishResponse", response); + invoke("publishText", responseText); } return response; diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 3a9de36b24..908806f2a1 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1134,7 +1134,15 @@ public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChan log.error("onStateChange {}", stateChange); String state = stateChange.current; - systemEvent("ON STATE %s", state); + + // getPeer("py4j") ? + // Py4j py4j +// String code = getName()".onStateChange(" +// invoke("publishPython", "onStateChange", stateChange ); + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", state); + } if (config.customSounds && customSoundMap.containsKey(state)) { invoke("publishPlayAudioFile", customSoundMap.get(state)); @@ -1167,6 +1175,10 @@ public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChan } return stateChange; } + +// public Message publishPython(String method, Object...data) { +// return Message.createMessage(getName(), getName(), method, data); +// } public OpenCVData onOpenCVData(OpenCVData data) { // FIXME - publish event with or without data ? String file reference diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java index fd2755a733..dd36bf505b 100644 --- a/src/main/java/org/myrobotlab/service/Py4j.java +++ b/src/main/java/org/myrobotlab/service/Py4j.java @@ -9,7 +9,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.bytedeco.javacpp.Loader; import org.myrobotlab.codec.CodecUtils; @@ -21,9 +20,11 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.net.Connection; import org.myrobotlab.service.config.Py4jConfig; import org.myrobotlab.service.data.Script; import org.myrobotlab.service.interfaces.Executor; +import org.myrobotlab.service.interfaces.Gateway; import org.slf4j.Logger; import py4j.GatewayServer; @@ -53,7 +54,7 @@ * * @author GroG */ -public class Py4j extends Service implements GatewayServerListener { +public class Py4j extends Service implements GatewayServerListener, Gateway { /** * POJO class to tie all the data elements of a external python process @@ -234,15 +235,6 @@ private String getClientKey(Py4JServerConnection gatewayConnection) { return String.format("%s:%d", gatewayConnection.getSocket().getInetAddress(), gatewayConnection.getSocket().getPort()); } - /** - * return a set of client connections - probably could be deprecated to a - * single client, but was not sure - * - * @return - */ - public Set getClients() { - return clients.keySet(); - } /** * get listing of filesystem files location will be data/Py4j/{serviceName} @@ -336,7 +328,19 @@ public boolean preProcessHook(Message msg) { // TODO - determine clients are connected .. how many clients etc.. try { if (handler != null) { - handler.invoke(msg.method, msg.data); + // afaik - Py4j does some kind of magical encoding to get a JavaObject + // back to the Python process, but: + // 1. its useless for users - no way to access the content ? + // 2. you can't do anything with it + // So, I've chosen to json encode it here, and the Py4j.py MessageHandler will + // decode it into a Python dictionary \o/ + // we do single encoding including the parameter array - there is no header needed + // with method and other details, as the invoke here is invoking directly in the + // Py4j.py script + + String json = CodecUtils.toJson(msg); + // handler.invoke(msg.method, json); + handler.send(json); } else { error("preProcessHook handler is null"); } @@ -586,7 +590,37 @@ public static void main(String[] args) { log.error("main threw", e); } } - - + + @Override + public void connect(String uri) throws Exception { + // host:port of python process running py4j ??? + + } + + /** + * Remote in this context is the remote python process + */ + @Override + public void sendRemote(Message msg) throws Exception { + log.info("sendRemote"); + String jsonMsg = CodecUtils.toJson(msg); + handler.send(jsonMsg); + } + + @Override + public boolean isLocal(Message msg) { + return Runtime.getInstance().isLocal(msg); + } + + @Override + public List getClientIds() { + return Runtime.getInstance().getConnectionUuids(getName()); + } + + @Override + public Map getClients() { + return Runtime.getInstance().getConnections(getName()); + } + } diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index ca547d15f6..6d4354f121 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -33,7 +33,7 @@ public class InMoov2Config extends ServiceConfig { /** * enable custom sound map for state changes */ - public boolean customSound = false; + public boolean customSounds = false; public boolean forceMicroOnIfSleeping = true; @@ -121,6 +121,11 @@ public class InMoov2Config extends ServiceConfig { */ public boolean systemEventsOnBoot = false; + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; + /** * */ @@ -225,6 +230,12 @@ public Plan getDefault(Plan plan, String name) { chatBot.listeners = new ArrayList<>(); } chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + + + ProgramABConfig gpt3 = (ProgramABConfig) plan.get(getPeerName("gpt3")); + gpt3.listeners = new ArrayList<>(); + gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + HtmlFilterConfig htmlFilter = (HtmlFilterConfig) plan.get(getPeerName("htmlFilter")); // htmlFilter.textListeners = new String[] { name + ".mouth" }; diff --git a/src/main/java/org/myrobotlab/service/interfaces/Executor.java b/src/main/java/org/myrobotlab/service/interfaces/Executor.java index e1566c015a..85ebdb61d1 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/Executor.java +++ b/src/main/java/org/myrobotlab/service/interfaces/Executor.java @@ -1,6 +1,7 @@ package org.myrobotlab.service.interfaces; -import org.myrobotlab.framework.interfaces.Invoker; +import org.myrobotlab.framework.interfaces.JsonInvoker; +import org.myrobotlab.framework.interfaces.JsonSender; /** * Interface to a Executor - currently only utilized by Py4j to @@ -10,7 +11,7 @@ * @author GroG * */ -public interface Executor extends Invoker { +public interface Executor extends JsonInvoker, JsonSender { /** * exec in Python - executes arbitrary code diff --git a/src/main/resources/resource/Py4j/Py4j.py b/src/main/resources/resource/Py4j/Py4j.py index 0cdb5b3bce..3c75cd94c5 100644 --- a/src/main/resources/resource/Py4j/Py4j.py +++ b/src/main/resources/resource/Py4j/Py4j.py @@ -13,13 +13,78 @@ # the gateway import sys - +import json +from abc import ABC, abstractmethod from py4j.java_collections import JavaObject, JavaClass from py4j.java_gateway import JavaGateway, CallbackServerParameters, GatewayParameters -runtime = None +class Service(ABC): + def __init__(self, name): + self.java_object = runtime.start(name, self.getType()) + + def __getattr__(self, attr): + # Delegate attribute access to the underlying Java object + return getattr(self.java_object, attr) + + def __str__(self): + # Delegate string representation to the underlying Java object + return str(self.java_object) + + def subscribe(self, event): + print("subscribe") + self.java_object.subscribe(event) + + @abstractmethod + def getType(self): + pass + + +class NeoPixel(Service): + def __init__(self, name): + super().__init__(name) + + def getType(self): + return "NeoPixel" + + def onFlash(self): + print("onFlash") + + +class InMoov2(Service): + def __init__(self, name): + super().__init__(name) + self.subscribe('onStateChange') + + def getType(self): + return "InMoov2" + + def onOnStateChange(self, state): + print("onOnStateChange") + print(state) + print(state.get('last')) + print(state.get('current')) + print(state.get('event')) + + +# TODO dynamically add classes that you don't bother to check in +# class Runtime(Service): +# def __init__(self, name): +# super().__init__(name) + + +# FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!! +runtime = None + +# TODO - rename to mrl_lib ? +# e.g. +# mrl = mrl_lib.connect("localhost", 1099) +# i01 = InMoov("i01", mrl) +# or +# runtime = mrl_lib.connect("localhost", 1099) # JVM connection Py4j instance needed for a gateway +# runtime.start("i01", "InMoov2") # starts Java service +# runtime.start("nativePythonService", "NativePythonClass") # starts Python service no gateway needed class MessageHandler(object): """ The class responsible for receiving and processing Py4j messages, @@ -41,10 +106,34 @@ def __init__(self): python_server_entry_point=self, gateway_parameters=GatewayParameters(auto_convert=True)) self.runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance() + # FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!! runtime = self.runtime self.py4j = None # need to wait until name is set print("initialized ... waiting for name to be set") + def construct_runtime(self): + """ + Constructs a new Runtime instance and returns it. + """ + jvm_runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance() + + # Define class attributes and methods as dictionaries + class_attributes = { + 'x': 0, + 'y': 0, + 'move': lambda self, dx, dy: setattr(self, 'x', self.x + dx) or setattr(self, 'y', self.y + dy), + 'get_position': lambda self: (self.x, self.y), + } + + # Create the class dynamically using the type() function + MyDynamicClass = type('MyDynamicClass', (object,), class_attributes) + + # Create an instance of the dynamically created class + obj = MyDynamicClass() + + + return self.runtime + # Define the callback function def handle_connection_break(self): # Add your custom logic here to handle the connection break @@ -78,11 +167,15 @@ def setName(self, name): print("reference to runtime") # TODO print env vars PYTHONPATH etc return name + + def getRuntime(self): + return self.runtime def exec(self, code): """ Executes Python code in the global namespace. - All exceptions are caught and printed so that the Python subprocess doesn't crash. + All exceptions are caught and printed so that the + Python subprocess doesn't crash. :param code: The Python code to execute. :type code: str @@ -94,22 +187,38 @@ def exec(self, code): except Exception as e: print(e) - def invoke(self, method, data=()): + def send(self, json_msg): + msg = json.loads(json_msg) + if msg.get("data") is None or msg.get("data") == []: + globals()[msg.get("method")]() + else: + globals()[msg.get("method")](*msg.get("data")) + + # equivalent to JS onMessage + def invoke(self, method, data=None): """ Invoke a function from the global namespace with the given parameters. :param method: The name of the function to invoke. :type method: str - :param data: The parameters to pass to the function, defaulting to no parameters. + :param data: The parameters to pass to the function, defaulting to + no parameters. :type data: Iterable """ # convert to list - params = list(data) + # params = list(data) not necessary will always be a json string # Lookup the method in the global namespace # Much much faster than using eval() - globals()[method](*params) + + # data should be None or always a list of params + if data is None: + globals()[method]() + else: + # one shot json decode + params = json.loads(data) + globals()[method](*params) def shutdown(self): """ diff --git a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js index 0e1e8f7ce5..42c559c999 100644 --- a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js @@ -49,8 +49,8 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta _self.updateState(data) $scope.$apply() break - case 'onNewState': - $scope.current = data + case 'onStateChange': + $scope.current = data.current $scope.$apply() break default: @@ -60,7 +60,7 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta } - msg.subscribe("publishNewState") + msg.subscribe("publishStateChange") msg.subscribe(this) } ]) diff --git a/src/main/resources/resource/WebGui/app/service/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html index 4abedeb490..4557717bdc 100644 --- a/src/main/resources/resource/WebGui/app/service/tab-header.html +++ b/src/main/resources/resource/WebGui/app/service/tab-header.html @@ -55,7 +55,7 @@
  • - +   subscriptions