################################
# Py4j.py
# more info here: https://www.py4j.org/
# Py4J enables Python programs running in a Python interpreter to dynamically access
# Java objects in a Java Virtual Machine.
# Methods are called as if the Java objects resided in the Python interpreter and
# Java collections can be accessed through standard Python collection methods.
# Py4J also enables Java programs to call back Python objects. Py4J is distributed under the BSD license
# Python 2.7 -to- 3.x is supported
# In your python 3.x project
# pip install py4j
# you have full access to mrl instance that's running
# the gateway
import json
import sys
from abc import ABC, abstractmethod
from py4j.java_collections import JavaClass, JavaObject
from py4j.java_gateway import CallbackServerParameters, GatewayParameters, JavaGateway
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,
including handling `invoke()` and `exec()` requests. Class
must be initialized and then the `setName()` method must be invoked before
the Java and Python sides can talk correctly.
"""
def __init__(self):
global runtime
# initializing stdout and stderr
print("initializing")
self.name = None
self.stdout = sys.stdout
self.stderr = sys.stderr
sys.stdout = self
sys.stderr = self
self.gateway = JavaGateway(
callback_server_parameters=CallbackServerParameters(),
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
print("Connection with Java gateway was lost or terminated.")
print("goodbye.")
sys.exit(1)
def write(self, string):
if self.py4j:
self.py4j.handleStdOut(string)
def flush(self):
pass
def setName(self, name):
"""
Called after initialization completed in order
for this Python side handler to know how to contact the Java-side
service.
:param name: Name of the Java-side Py4j service this script is linked to, preferably as a full name.
:type name: str
"""
print("name set to", name)
self.name = name
self.py4j = self.runtime.getService(name)
print(self.runtime.getUptime())
print("python started", sys.version)
print("runtime attached", self.runtime.getVersion())
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.
:param code: The Python code to execute.
:type code: str
"""
try:
# Restricts the exec() to the global namespace,
# so the code is executed as if it were the top level of a module
exec(code, globals())
except Exception as e:
print(e)
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.
:type data: Iterable
"""
# convert to list
# params = list(data) not necessary will always be a json string
# Lookup the method in the global namespace
# Much much faster than using eval()
# 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):
"""
Shutdown the Py4j gateway
:return:
"""
self.gateway.shutdown()
def convert_array(self, array):
"""
Utility method used by Py4j to convert arrays of Java objects
into equivalent Python lists.
:param array: The array to convert
:return: The converted array
"""
result = []
for item in array:
if isinstance(item, JavaObject):
item_class = item.getClass()
if item_class == JavaClass.forName("java.lang.String"):
result.append(str(item))
elif item_class == JavaClass.forName("java.lang.Integer"):
result.append(int(item))
elif item_class == JavaClass.forName("java.lang.Double"):
result.append(float(item))
elif item_class == JavaClass.forName("java.lang.Boolean"):
result.append(bool(item))
elif item_class == JavaClass.forName("java.lang.Long"):
result.append(int(item))
elif item_class == JavaClass.forName("java.lang.Short"):
result.append(int(item))
elif item_class == JavaClass.forName("java.lang.Float"):
result.append(float(item))
elif item_class == JavaClass.forName("java.lang.Byte"):
result.append(int(item))
else:
raise ValueError("Unsupported type: {}".format(item_class))
else:
raise ValueError("Unsupported type: {}".format(type(item)))
return result
class Java:
implements = ["org.myrobotlab.framework.interfaces.Invoker"]
handler = MessageHandler()
if len(sys.argv) > 1:
handler.setName(sys.argv[1])
else:
raise RuntimeError(
"This script requires the full name of the Py4j service as its first command-line argument"
)