Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ pip install rocketserializer
### Java

You need Java to be installed on your system to use `rocketserializer`.
We recommend downloading Java 17, which is required to run OpenRocket-23.09.
We recommend downloading Java 17, which is required to run recent OpenRocket
JARs (for example OpenRocket-24.12).

https://www.oracle.com/java/technologies/downloads/

Expand All @@ -47,7 +48,7 @@ https://www.oracle.com/java/technologies/downloads/
You also need to download the OpenRocket JAR file. You can download it from the
following link:

https://openrocket.info/downloads.html?vers=23.09#content-JAR
https://openrocket.info/downloads.html

Each version of OpenRocket has its own jar file, and it is important to use the
correct java version to run the jar file.
Expand All @@ -61,7 +62,7 @@ will be automatically installed:
- click>=8.0.0
- lxml
- numpy
- orhelper==0.1.3
- jpype1<1.5
- pyyaml
- rocketpy>=1.1.0
- nbformat>=5.2.0
Expand Down Expand Up @@ -89,7 +90,7 @@ The options are the following:

- `--filepath`: The .ork file to be serialized.
- `--output` : Path to the output folder. If not set, the output will be saved in the same folder as the `filepath`.
- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will try to find the jar file in the current directory.
- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will use the newest `OpenRocket*.jar` found in the current directory.
- `--encoding` : The encoding of the .ork file. By default, it is set to `utf-8`.
- `--verbose` : If you want to see the progress of the serialization, set this option to True. By default, it is set to False.

Expand Down Expand Up @@ -139,4 +140,4 @@ The 3 main ways of contributing to this project are:
- If you allow us to use and share your .ork file, we can add it to the test suite.
3. **Developing new features and fixing bugs thorough pull requests on GitHub.**
- If you want to develop new features, you are more than welcome to do so.
- Please reach out to the maintainers to discuss the new feature before starting the development.
- Please reach out to the maintainers to discuss the new feature before starting the development.
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ bs4
click>=8.0.0
lxml
numpy
orhelper==0.1.3
jpype1<1.5
pyyaml
rocketpy>=1.1.0
nbformat>=5.2.0
61 changes: 26 additions & 35 deletions rocketserializer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from pathlib import Path

import click
import orhelper

from ._helpers import extract_ork_from_zip, parse_ork_file
from .nb_builder import NotebookBuilder
from .openrocket_runtime import OpenRocketSession, select_latest_openrocket_jar
from .ork_extractor import ork_extractor

logging.basicConfig(
Expand Down Expand Up @@ -138,18 +138,15 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals
raise ValueError(message)

if not ork_jar:
# get any .jar file in the current directory that starts with "OpenRocket"
ork_jar = [
f for f in os.listdir() if f.startswith("OpenRocket") and f.endswith(".jar")
]
if len(ork_jar) == 0:
raise ValueError(
"[ork2json] It was not possible to find the OpenRocket .jar file in "
"the current directory. Please specify the path to the .jar file."
)
ork_jar = ork_jar[0]
logger.info(
"[ork2json] Found OpenRocket .jar file: '%s'", Path(ork_jar).as_posix()
ork_jar = select_latest_openrocket_jar(Path.cwd())
logger.info("[ork2json] Found OpenRocket .jar file: '%s'", ork_jar.as_posix())
else:
ork_jar = Path(ork_jar)

if not ork_jar.exists():
raise FileNotFoundError(
"[ork2json] The specified OpenRocket .jar file does not exist: "
f"'{ork_jar.as_posix()}'"
)

if not output:
Expand All @@ -160,17 +157,12 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals
Path(output).as_posix(),
)

# orhelper options are: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL
# log_level = "OFF" if verbose else "OFF"
# TODO: even if the log level is set to OFF, the orhelper still prints msgs

with orhelper.OpenRocketInstance(ork_jar, log_level="OFF") as instance:
with OpenRocketSession(ork_jar, log_level="OFF") as instance:
# create the output folder if it does not exist
if os.path.exists(output) is False:
os.mkdir(output)
Comment thread
Gui-FernandesBR marked this conversation as resolved.
Outdated

orh = orhelper.Helper(instance)
ork = orh.load_doc(str(filepath))
ork = instance.load_doc(str(filepath))

settings = ork_extractor(
bs=bs,
Expand Down Expand Up @@ -217,21 +209,20 @@ def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False
"[ork2notebook] Output folder not specified. Using '%s' instead.",
Path(output).as_posix(),
)
ork2json(
[
"--filepath",
filepath,
"--output",
output,
"--ork_jar",
ork_jar,
"--encoding",
encoding,
"--verbose",
verbose,
],
standalone_mode=False,
)
args = [
"--filepath",
str(filepath),
"--output",
str(output),
"--encoding",
str(encoding),
"--verbose",
str(verbose),
]
if ork_jar:
args.extend(["--ork_jar", str(ork_jar)])

ork2json(args, standalone_mode=False)

instance = NotebookBuilder(parameters_json=os.path.join(output, "parameters.json"))
instance.build(destination=output)
237 changes: 237 additions & 0 deletions rocketserializer/openrocket_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import logging
import os
import re
from pathlib import Path

import jpype
import jpype.imports

logger = logging.getLogger(__name__)


def _jar_version_tuple(jar_path: Path):
match = re.search(r"OpenRocket[-_]?(\d+(?:\.\d+)*)", jar_path.name, re.IGNORECASE)
if not match:
return (0,)

version = []
for token in match.group(1).split("."):
if token.isdigit():
version.append(int(token))
return tuple(version) if version else (0,)


def select_latest_openrocket_jar(search_dir: Path):
jars = [
path
for path in search_dir.iterdir()
if path.is_file()
and path.suffix.lower() == ".jar"
and path.name.lower().startswith("openrocket")
]

if not jars:
raise FileNotFoundError(
"It was not possible to find an OpenRocket .jar file in the current "
"directory. Please specify one explicitly with --ork_jar."
)

jars.sort(
key=lambda path: (_jar_version_tuple(path), path.name.lower()), reverse=True
)
return jars[0]


def _extract_java_major(value: str):
if not value:
return None

normalized = value.replace("\\", "/").lower()

legacy = re.search(r"(?:jdk|jre)[-_]?1\.(\d+)", normalized)
if legacy:
return int(legacy.group(1))

modern = re.search(r"(?:jdk|jre|java)[-_]?(\d{2})", normalized)
if modern:
return int(modern.group(1))

return None


def _minimum_java_required(jar_path: Path):
version = _jar_version_tuple(jar_path)
if version and version[0] >= 23:
return 17
return 8


def _find_windows_jdk(minimum_major: int):
search_roots = [
Path("C:/Program Files/Java"),
Path("C:/Program Files/Eclipse Adoptium"),
Path("C:/Program Files/AdoptOpenJDK"),
]

candidates = []
for root in search_roots:
if not root.exists():
continue

for child in root.iterdir():
if not child.is_dir():
continue
major = _extract_java_major(child.name)
if major is None:
continue
if major >= minimum_major and (child / "bin" / "java.exe").exists():
candidates.append((major, child))

if not candidates:
return None

candidates.sort(key=lambda item: item[0], reverse=True)
return candidates[0][1]


def ensure_java_compatibility(jar_path: Path):
required_major = _minimum_java_required(jar_path)
if required_major <= 8:
return

java_home_major = _extract_java_major(os.environ.get("JAVA_HOME", ""))
if java_home_major and java_home_major >= required_major:
return

default_jvm_major = None
try:
default_jvm_major = _extract_java_major(jpype.getDefaultJVMPath())
except Exception:
default_jvm_major = None

if default_jvm_major and default_jvm_major >= required_major:
return

if os.name != "nt":
logger.warning(
"OpenRocket %s requires Java %d+, but no compatible JVM was detected.",
jar_path.name,
required_major,
)
return

selected_jdk = _find_windows_jdk(required_major)
if not selected_jdk:
logger.warning(
"OpenRocket %s requires Java %d+, but no compatible JDK was found in "
"standard Windows locations.",
jar_path.name,
required_major,
)
return

os.environ["JAVA_HOME"] = str(selected_jdk)
os.environ["PATH"] = (
str(selected_jdk / "bin") + os.pathsep + os.environ.get("PATH", "")
)
logger.info(
"Using Java from '%s' for OpenRocket compatibility.", selected_jdk.as_posix()
)


class OpenRocketSession:
def __init__(self, jar_path, log_level="OFF"):
self.jar_path = Path(jar_path)
if not self.jar_path.exists():
raise FileNotFoundError(
f"Jar file '{self.jar_path.as_posix()}' does not exist"
)

self.log_level = log_level
self.openrocket = None
self.started = False

def _resolve_packages(self):
try:
legacy = jpype.JPackage("net").sf.openrocket
_ = legacy.startup.Application
return legacy, legacy
except Exception:
modern = jpype.JPackage("info").openrocket
return modern.core, modern.swing

@staticmethod
def _block_loader(gui_module, field_name):
try:
field = gui_module.getClass().getDeclaredField(field_name)
field.setAccessible(True)
loader = field.get(gui_module)
field.setAccessible(False)
loader.blockUntilLoaded()
except Exception:
# New OpenRocket versions can change internals; loading still works
# without explicitly waiting in most cases.
return

def __enter__(self):
ensure_java_compatibility(self.jar_path)

jvm_path = jpype.getDefaultJVMPath()
logger.info(
"Starting JVM from '%s' with OpenRocket '%s'",
jvm_path,
self.jar_path.as_posix(),
)

jpype.startJVM(
jvm_path,
"-ea",
f"-Djava.class.path={self.jar_path.as_posix()}",
)
Comment thread
Gui-FernandesBR marked this conversation as resolved.
Outdated

self.openrocket, swing = self._resolve_packages()

guice = jpype.JPackage("com").google.inject.Guice
logger_factory = jpype.JPackage("org").slf4j.LoggerFactory
logger_class = jpype.JPackage("ch").qos.logback.classic.Logger
logger_level = jpype.JPackage("ch").qos.logback.classic.Level

gui_module = swing.startup.GuiModule()
plugin_module = self.openrocket.plugin.PluginModule()

injector = guice.createInjector(gui_module, plugin_module)

app = self.openrocket.startup.Application
app.setInjector(injector)

gui_module.startLoader()
self._block_loader(gui_module, "presetLoader")
self._block_loader(gui_module, "motorLoader")

root_logger = logger_factory.getLogger(logger_class.ROOT_LOGGER_NAME)
root_logger.setLevel(
getattr(logger_level, str(self.log_level), logger_level.ERROR)
)

self.started = True
return self

def __exit__(self, ex_type, ex, tb):
try:
if jpype.isJVMStarted():
try:
for window in jpype.java.awt.Window.getWindows():
window.dispose()
except Exception:
pass
jpype.shutdownJVM()
Comment thread
Gui-FernandesBR marked this conversation as resolved.
Outdated
finally:
self.started = False

def load_doc(self, ork_filename: str):
if not self.started:
raise RuntimeError("OpenRocketSession has not been started")

java_file = jpype.java.io.File(ork_filename)
loader = self.openrocket.file.GeneralRocketLoader(java_file)
return loader.load()
Loading
Loading