目录
CesarCoelho

Release 5.1 (#193)

  • Bump version from 5.0 to 5.1-SNAPSHOT for the next development cycle

Update every pom across both reactors (the main build and the detached testbeds), plus the version-tied references in CLAUDE.md, the Sphinx release, and the mission filesystem docs. RELEASE_NOTES.md keeps its 5.0 entry.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Rename Parameter/Action/Alert names to the dotted lowercase convention

Move every example-app and supervisor Parameter, Action, and Alert name to the “namespace.lowercase-with-dashes” scheme (e.g. AttitudeQuaternion_a -> adcs.attitude-quaternion.a, GPS_Latitude -> gps.latitude, takePicture_RAW -> camera.take-picture.raw), with axis/format components split by a dot and the ADCS attitude modes grouped under adcs.mode.*. Single-purpose demo apps with only a handful of names use bare names (e.g. amplitude, run-python-script).

Field/method-derived @Parameter/@Action names gain an explicit name= since a Java identifier cannot carry dots or dashes. Consumers are kept in sync: the ground examples, the space-to-space subscription list, the testbed assertions, and the supervisor docs.

OPS-SAT hardware channel IDs (CADC*, ORX*, …) are left as-is.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Point Maven Central badge at nanosat-mo-framework artifact

The badge tracked the removed int.esa.nmf:parent artifact, which is frozen at 4.0. Switch to the root reactor artifact int.esa.nmf:nanosat-mo-framework, whose Maven Central metadata reports 5.0, so the badge shows the current release.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Fill in missing Javadoc across the public API and core services

Document the app-facing and mission-integrator public surface, driving javadoc:javadoc to zero warnings for the scoped modules:

  • core/nmf-composites (app-facing API)
  • core/nmf-package-lib (package system)
  • core/nmf-package-maven-plugin, core/nmf-linux-maven-plugin
  • core/nmf-services-impl: the public service entry points (*ServiceImpl providers/consumers and the service-stack util classes)

Adds constructors/fields/methods/enum-constant Javadoc, uses private constructors for all-static utility classes, and places added constructors after the field declarations.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Simplify MCRegistration constructor and encapsulate its fields

Replace the five-argument MCRegistration constructor with MCRegistration(COMServicesProvider, MCServicesProviderNMF), deriving the four MC service providers from the aggregate. Make the fields private and expose the two the framework needs via getCOMServices()/getParameterService(), and update MonitorAndControlNMFAdapter and the three provider composites (NanoSatMOConnectorImpl, NanoSatMOMonolithic, NanoSatMOSupervisor) plus the ParameterTest accordingly.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Add Javadoc to remaining api-nmf-com and mission/demo entry points

Document COMObject and COMService (class, constructors, accessors) in api-nmf-com, add a documented constructor to BareboneSupervisorImpl, and a private constructor to the all-static MonolithicProviderDemo. Clears the last hand-written Javadoc warnings outside the simulator, CTT, examples and service-impl internals.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Fill in missing Javadoc for the cli-tool module

Document the CLI-Tool: BaseCommand and the command containers, every nested XxxCommand (referencing its actual CLI group/subcommand), the ExitCodes constants, the Args parser, the Timestamped value holders, the archive adapters, and the CLITool/Dispatcher/Helper utilities. Static-only classes get private constructors. Drives javadoc:javadoc for sdk/cli-tool to zero warnings.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Add Javadoc to the hand-written members of consumer-test-tool

Document the CLI-Tool GUI’s hand-written classes, constructors, methods and protected/public fields: the service consumer/table panels, the add/modify dialogs, the MO-element form-field editors, the archive adapters and the value labels. Private fields are intentionally left undocumented (they are implementation detail, not API), and the NetBeans //GEN-BEGIN generated blocks are left untouched.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Add Javadoc to the public API of the example apps

Document the public/protected members across all sdk/examples-apps and sdk/examples-ground modules: app entry points, Monitor and Control adapters, action handlers, parameter suppliers, getters, config constants, enums and utility classes. Private members are intentionally left undocumented.

Also fix a pre-existing typo in the method name FileUtils.newOutpuStreamSafe -> newOutputStreamSafe (and its call sites) and its documentation.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Add Javadoc to nmf-services-impl internals and remove dead code

Document the public/protected members of the internal helper classes (the register-map helpers, the definition managers, the COM archive/DB layer, decoders, and environment utilities), leaving private members undocumented. All-static utility classes get private constructors, abstract classes get protected ones.

Also remove dead code: the unused COMObjectEntity.generatePK(), COMObjectEntity.getPrimaryKey() and the now-unreferenced COMObjectEntityPK class (a leftover from the removed JPA/eclipse-link persistence).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Rename and prune CubeSat Simulator stub peripherals

The simulator registered nine device tabs, but four were non-functional stubs: no @ISimulatorDeviceData descriptors (blank tab) and no command handler (runRawCommand fell through to the switch default). Clean these up:

  • Rename MityARM -> DPU (PMityARM/IMityARM -> PDPU/IDPU, tab “DPU”).
  • Rename CCSDSEngine -> On-Board Router (PCCSDSEngine/ICCSDSEngine -> POnBoardRouter/IOnBoardRouter, tab “On-Board Router”).
  • Remove FDIR and Nanomind (peripheral + interface + registration).

The INTERFACE_* constants are positional indices into the device list, so removing FDIR and Nanomind shifted the later devices; renumber the constants to keep them aligned (INTERFACE_FINEADCS is used throughout). Command internalIDs are unaffected: dispatch is a direct switch on the explicit ID, not on device position.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Report the true reason when the simulator serial filter rejects a stream

The rejection logging trusted ObjectInputFilter.FilterInfo.serialClass() to decide why a stream was rejected. But when the JDK pattern filter trips a resource limit (maxdepth/maxrefs/maxbytes/maxarray) it returns REJECTED with serialClass() still set to whatever class was being read at that moment. The old code therefore misreported a limit breach as a class rejection - e.g. “rejected class java.lang.Integer” (an allowlisted keepalive) when the real cause was the cumulative maxrefs/maxbytes of a long-lived server stream being reached - and advised allowlisting a class that was already allowed.

Detect limit breaches explicitly by comparing the configured limits against the reported counters, and only report a class-not-in-allowlist when no limit was breached. Always include the full counter snapshot (depth, refs, bytes, arrayLength, class) so any rejection is diagnosable. The limits are pulled into named numeric constants so the logger can compare against them.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Drop cumulative maxrefs/maxbytes from the simulator serial filters

The JDK enforces maxrefs and maxbytes cumulatively over the whole ObjectInputStream, not per message. The transport keeps a single stream open for a peer’s entire connection, so those counters only climb and any finite value is eventually reached: on a long-lived connection the server rejected legitimate traffic (an allowlisted Integer keepalive) once maxrefs passed 2000, roughly every few minutes, then reconnected and repeated.

Remove maxrefs and maxbytes from both the server and client filters (which now share a single LIMITS = maxdepth + maxarray). The per-message bounds that actually bound one message are retained (maxdepth caps graph depth, maxarray caps any single array), and the class allowlist - the real defense against gadget-chain RCE (CWE-502) - is unchanged. The rejection logging keeps reporting the true reason and still prints the refs/bytes counters for visibility even though they are no longer enforced.

Update SimulatorSerialFilterTest: the case that asserted the server rejects a high-reference-count message encoded the removed behaviour; it now asserts both filters accept it, guarding against re-introducing the cumulative limit.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

  • Remove unused imports from services impl, simulator and example apps

Drops 24 imports that no longer resolve to any code reference in core/nmf-services-impl, mission/simulator and sdk/examples-apps. The five removed from GPSSoftSimAdapterTest backed commented-out test bodies only.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Report the config file of the bootloader by its path alone

The initialisation line read “config: loaded from

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Reach a running App straight from the Apps Launcher service

Connecting to an App meant going back to the Directory tab and looking it up by hand, although the Apps Launcher panel already receives everything it needs: it subscribes to the output of each App, and an App prints the URI of its own Directory service once it is up.

A “connect to App” button now sits at the front of the panel. It follows the selected App and is offered only once that App has said where it can be reached, which is why it appears by itself a moment after an App is started. Asking an App to stop, or killing it, takes the button away again, and starting the App once more brings it back.

The URI is picked out of the output by a pattern built around the name of the App, because the output carries other URIs that must not be taken for it: the Central Directory of the supervisor, which an App reports while registering itself, and the connections that an App closes while it shuts down. An App names its own provider after itself, so the name tells the two apart.

Connecting is the same connection that the Directory tab makes, moved into a method that both call, so an App that offers a Login service is still logged into first and its tab still closes the same way.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Build the path of an extracted file rather than checking it afterwards

A package names the files it carries, and anyone can build a package. A name such as “../../etc/passwd” joined to the folder being extracted to would write outside it, which is the attack known as Zip Slip. The names were joined and the result was then checked against the folder, so the path that leaves the folder was built first and caught second.

The name of an entry no longer becomes a path. It is taken apart at the separators and each piece has to be a plain file name before it is appended, so a piece that climbs out of the folder, or starts again from a root, is refused rather than appended. Appending plain names can only go downwards, so the file lands under the folder by construction and there is nothing left to check.

A name of nothing but dots is refused as a whole rather than just “..”, because Windows drops the dots at the end of a name and a longer run of them can still be read as the folder above once the file is opened.

A name that starts from a root is stripped of it rather than refused, so that “/etc/passwd” is kept as a file of the package instead of failing the install.

The check that this replaces also compared a canonical path against one that was only normalized, so extracting into a folder reached through a symbolic link would have failed although nothing was wrong. The folder is now resolved once, before the files are walked.

The tests hold every way of writing a name that leaves the folder, and they fail if the old behaviour comes back.

Reported by CodeQL as java/zipslip.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Work out where a package writes its files in one place only

The files of a package were reached in three places, each joining the name of an entry to a folder and each guarding against the names that leave it by refusing the ones that hold “..”. Removing a package walked that path to delete files, so a name that left the folder deleted files that no package had put there.

All three now ask for the path the same way, so a name that leaves the folder is refused where the path is built rather than in each caller, and the way of writing that refusal lives in one place. sanitizePath is gone with them: it read the name as a whole and refused it for holding “..”, which is a list of one spelling, and it left the caller to join what it returned.

Removing a package also joined the folder and the name as text rather than as paths, so a name holding a separator of the other system was read as one long name instead of as a path.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say that the bubblewrap and Docker isolation modes are implemented

Three places said that apps could not yet be isolated in the ways they can: the constant, the parameter of the Maven plugin that sets it, and the page that explains the modes. The plugin is where a mission chooses one, and it named two of the four modes as not implemented, so a mission had no reason to try either.

The page now describes what the Supervisor does for the Docker mode, as it already did for the others, and says what can be configured: the image the apps run in and the network the containers join. It also says that reaching the Docker daemon is as good as being root on the host, so that a mission weighs the boundary it gives up against the one it gains.

The table of modes left the Docker mode out and told the reader which mode to pick, which does not survive contact with a real mission: it called the weaker of two modes the one for production. It now says where each mode applies and leaves the choice where it belongs.

The mode is also ignored on Windows, which the Supervisor has always done and nobody had written down.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Name the sections of the documentation after what they hold

“Mission Integration” is now “Specific Missions”, and each mission is named after itself rather than announcing itself as a page of its own, so that the two read as what they are: the missions under that section.

“Reference” is now “Technical Reference”, which says what separates it from “Background & Reference Documents” sitting beside it. The two were hard to tell apart by name although one is the live documentation and the other the formal documents of ESA.

“Removed Features” moves in beside them, and the section that held it alone is gone. It is documentation kept for those maintaining older deployments, which is what the other two are as well.

The pages that point at any of these carry the name of what they point at, so their sentences were read again and one of them shortened: it had come to say “applied to OPS-SAT, see OPS-SAT”.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Open the App Isolation page with the table of the modes

The table that sets the four modes against one another sat at the end, after each of them had been described, so a reader met the detail of all four before learning which one their mission calls for. It now opens the page, under Overview, and a reader can go straight to the mode that matches the hardware they have.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Write down what is missing from the NMF Package integrity check

The packages carry no signature: the signing block of the NMFPackageBuilder is commented out and the NMFDigitalSignature has no callers. The SHA256SUMS files that the Bootloader verifies are themselves unsigned, so they catch a corrupted download but not a tampered one.

Note it next to the integrity check that is still to be written, along with the Post-Quantum Cryptography that the signature would have to be based on.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Clear out the stale notes of the Parameter and Action managers

The setValues loop kept a block of commented-out code and a note asking what becomes of the newly created value. The value is added to the list right below it, so the question is already answered. Look the definition up once while there.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Look up the logs of an App in the domain that the App is in

The domain of the App was dropped and searched for as “*” instead, to work around the Archive Sync encoding the source domain in the wrong format. That was fixed five minutes later, in the commit right after the one that put the workaround in, and it has held ever since.

The wildcard matches every domain, so an Archive holding more than one of them could answer with the logs of another App that happens to carry the same instance id.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Drop the Aggregation listener that never had anything to hand over

The SimpleAggregationReceivedListener was added in 2017 with the loop that fills its list left unwritten, so it read the id and the value of each parameter and dropped both. Every notify called it with an empty list, and nothing on any branch ever implemented it.

Removing it turns a callback that quietly says nothing into a compile error. The CompleteAggregationReceivedListener already carries the whole AggregationValue, which is more than this one would have carried.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take out the note about caching the strings of the Binary Encoder

The note is from 2017, when the cost of turning a String into UTF-8 bytes was worth avoiding. It is an intrinsic since Java 9, and the strings that reach the Archive are not a bounded set, so the HashMap it asks for would grow without end.

Also wrap two of the Ground MO Adapter constructors the same way.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop the MO Adapter from reaching for the listener that was removed

The commit that dropped the SimpleAggregationReceivedListener left the two places that name it behind, so the build was broken between the two.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Report the angular velocity on the right axes and bound the GPS wait

The Y and Z components of the angular velocity were pushed onto each other’s parameter, unlike the sun vector and the magnetic field right next to them.

Reading the satellites in view waited on a semaphore that only the answer released, so an error from the GPS service, or no answer at all, held the thread that samples the parameters and with it every other parameter. The wait now ends on its own, the error is answered, and the count is only read once there is one.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop the Monolithic from warning about files it never had

The Supervisor sets the transport itself and the NMF Apps are installed with a provider.properties and a transport.properties of their own, but the Monolithic was left asking the HelperMisc for files that are not part of its deployment, so every start reported both as missing.

The two warnings were only noise, as the HelperMisc falls back to the same TCP/IP transport that it was already going to use. Read the files when they are there and fall back quietly when they are not, so a Monolithic that is deployed with them still has them read.

Move the defaults into the NMFProvider, where the Supervisor now takes them from as well, rather than holding a copy of its own.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Give the Monolithic project its name back

The project was moved out of the examples of the SDK, where it had been put, but it kept the name it carried there. Its README and its main class both call it the Monolithic Provider, so the module is named after it.

The assembly still pointed at a esa.mo.nmf.apps.DemoTestApp that is nowhere in the repository, another leftover of the move, so the jar was built naming a class that does not exist. Point it at the class that is actually there.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Move the Monolithic demo out of the package of the NMF Apps

The class sat in esa.mo.nmf.apps, which it shared across module bounds with the MCAllInOneAdapter of the examples, from the time it was kept among them. It is a provider rather than an App, so it is given a package of its own, named after the module it belongs to. The adapter it uses is now reached through an import.

The package of the Simulator binding, esa.mo.nmf.provider, would have spread one package over two Jars, so it was left alone.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take the schema version of the baseline files from one place

BootloaderGenerator carried its own copy of the literal that SoftwareBaseline already declared, so the version of the script-to-baseline-files contract was written down twice in Java and the two could drift apart without anything noticing. The generator now reads the constant from SoftwareBaseline, which the module already has on its compile classpath through nmf-package-lib.

The bootloader script keeps a copy of its own, as it has no way to reach a Java constant, so it stays the one place that has to be kept in step by hand.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Ship the NMF core baseline as a package of its own

The goal that builds an NMF core package was written but never called from anywhere, so no release ever carried one and the upgrade path it exists for was left to hand-assembled directories of Jars. A module now calls it, and every build produces an nmf-core .nmfpack beside the Jars.

The module takes a single dependency, on nmf-composites, because the runtime tree of that artifact together with the artifact itself is the set of Jars a generated filesystem places in jars-nmf/. Listing them one by one would be a second copy of that set, free to drift from the first. They are collected with copy-dependencies and handed to the goal, whose default location is left behind: it looks under the generated filesystem, and this module belongs to no mission and so generates none.

The package stages the Jars in a versioned directory beside the running one and does not activate them; that stays a deliberate step taken on-board with the setPrimaryBaseline action.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Test that the framework can be upgraded and rolled back end to end

The bootloader was covered for the versions it selects, but nothing drove a whole upgrade: booting a released version, installing a newer one beside it, switching to it and coming back. Two test classes now do, on the versions 5.0 and 5.1-SNAPSHOT.

The rollback test boots 5.0, upgrades to 5.1-SNAPSHOT, returns to 5.0, and leaves both installed. Every step asks the running Supervisor for nmf.version over MO rather than reading the files that were just written, and checks that nmf.uptime is small enough to prove the process was restarted rather than left alone. The fallback test gives the bootloader a 5.1 that cannot start and expects the ladder to move to the secondary rung, with 5.0 running after the next boot.

Both work on a copy of the generated filesystem: they change baseline files and can leave a Supervisor of another version behind, which the tests written for a single version are not prepared for. The Jars of 5.0 are staged from the repository, apart from the dependencies of the module so that the two versions never meet on a classpath.

The parameter names of the Supervisor and the baseline file keys were made public so that the tests can name them without repeating the strings.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Record the version of the NMF that an App was built against

An App carries only its own Jar and takes the framework from the baseline that the bootloader selects, so the version it was compiled against and the version it runs on are set at different times and by different people. The package said nothing about the first of the two: nothing in it named a framework version, and the Package Manager therefore had no ground on which to turn an App away that could not run.

The App metadata now carries that version, under pack.app.nmf-version. The Mojo already asked for it and only used it to fail early when it was unset, so the value was there and was being dropped; it is now written down.

This makes the metadata format version 5. Packages written before it carry no such field and read back as null, which is the value the format has for “not known” and the one a check will have to let through for as long as packages built by version 4 are still installed. The type of a package is read from info.type from version 4 onwards, so a package of version 5 is still read correctly by the Package Manager of NMF 5.0.

The version stays out of the SDK apps’ own poms: it comes from the parent, through the esa.nmf.version property that the Mojo already reads.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Test that an App can be taken back to a released version and still run

Installing and upgrading an App were covered, but never the way back. The Package Management service has no rollback operation: going back is the upgrade operation pointed at an older package, which compares no versions and simply replaces what is installed with what the package holds. Nothing said whether that really worked.

A test now installs the App of the version under development, takes it back to the version released before it, and brings it forward again. It is the released package taken from the repository rather than a rebuild of it, so what is installed is what was published, and it is a release of the same major version, which is the only case the NMF supports: an App is compiled against one version of the framework and linked against whichever one the Bootloader selected.

Each step asks the receipt which version is installed and looks in the App directory for the Jar of that version and for the absence of the other, and then starts the App. Starting it is the assertion worth having, because the operation deletes the files of the installed version before it writes the older ones, so a rollback that only half worked would leave a receipt claiming success. The version started is asserted before the App is run, without which the test would pass whether or not anything was rolled back.

The test works on a copy of the filesystem, since it leaves the App on the version it finished with. The copy was taken out of the harness of the NMF upgrade tests, which needed it for the same reason, and which adds a second NMF baseline that an App test does not want: the start script of an App takes every Jar under jars-nmf, so a second baseline would put two versions of the framework on its classpath.

The version to install is named by Maven rather than read from the copy, and the version uninstalled is the one the copy came with, because the tests that run before this one leave the App on versions of their own.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Name the Broken App after the testbed it belongs to

The module called itself “ESA NMF Testbeds - Broken App” while the testbed it serves is named in the singular everywhere else.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Show the simulated spacecraft in Celestia

The simulator has long been able to feed a visualisation, behind a flag that was off and with nothing on the other end to feed. This is the other end: the spacecraft and the ground stations as Celestia objects, a script that flies them from the simulator, and a container so that Celestia does not have to be installed on anybody’s machine.

The script connects to the Celestia server of the simulator, answers each message as the protocol requires, and hands Celestia a position and an attitude whenever it asks where the spacecraft is. It never waits on the socket: a simulator that is not running costs a frame nothing and the spacecraft stays where it was last seen.

Celestia 1.6.4 rather than 1.7. The packaged 1.7 does not act on the ScriptedOrbit and ScriptedRotation entries of a spacecraft definition at all: the object is created and the module it names is never loaded, which was established with a definition written for the purpose. Those entries are the whole mechanism. 1.6.4 is published only up to jammy, which is why the image is built on jammy; the version of the machine it runs on does not matter.

Three things about Celestia are worth knowing and are written down in the README, because none of them announce themselves: the methods it looks for are called position and orientation, it searches for modules relative to its own directory rather than anywhere that can be mounted, and it says nothing whatsoever when any of it goes wrong.

A stand-in for the simulator is included, so that the Celestia side can be worked on without starting a Supervisor, a propagator and a mission.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Have the simulator offer its Celestia server without being asked

The server was off unless someone found the file the simulator writes for itself and changed a line in it. That file lives beside whatever directory the Supervisor happens to run from, which for the playground is under target, so any change to it was thrown away by the next build. The visualisation was therefore one rebuild away from being off again, without saying so.

The default now has it on. It only listens, and it costs nothing until something connects, so there is little reason for it to have been off. The propagator it depends on was already on by default.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Let the spacecraft be turned more than once

Commanding a new attitude worked the first time and then stopped. The mode asked for was recorded, reported by the ADCS service and believed by everything downstream, while the spacecraft carried on facing where it was. It was not that the change was refused: the sequence carrying the attitude completed its first transition and then, on the second, stopped arriving at the law it was moving to. It settled on no rotation at all and stayed there, so every command after the second was accepted and quietly ignored.

Each change is now carried by a sequence of its own, holding only the law being left and the law being taken up, and told to move between them a moment later. Nothing is carried over from one change to the next, so nothing accumulates, and the spacecraft turns over thirty seconds rather than jumping. The law being flown is tracked separately from the sequence that is carrying the spacecraft on to it, so a change always starts from a law rather than from the transition before it.

Two other things were wrong on the same path and are fixed here as well. A detector copied by Orekit, which is how it is given the handler that performs a switch, was rebuilt from nothing: it lost both the handler and the mode it was watching, leaving every detector watching the same mode and none of them able to switch anything. And setting new TLEs replaced the propagator without giving the new one the switch events and the additional state that the attitude depends on, which stopped the attitude changing from the next command onwards.

The tests command the attitude to change five times and check that it arrives where it was told each time, which is what stopped being true; and that a detector survives being copied.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Send the visualisation a position more often

Every third of a second was enough to show an orbit and not enough to show a spacecraft turning, which it now does over thirty seconds rather than instantly. It is produced every fifth of a second instead.

Little is gained by going below that. The Celestia server waits for an acknowledgement after every message and waits by sleeping in steps of 150 ms, so nothing produced more often than that can leave. The constant says so, to save the next person from trying.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Call the spacecraft by its name in Celestia

It was OPSSAT everywhere in the definition, which is not how the mission is written down anywhere else. Celestia shows the name beside the spacecraft, so it is the one thing here that is read by someone rather than by a program.

The propagated companion object goes with it, and so does the script that looks the spacecraft up on startup.

The identifier the simulator puts in its messages is left as it was. Nothing reads it: the client takes the parameters by name and pays no attention to which spacecraft they are said to belong to.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Start Celestia looking down at the spacecraft, not through the Earth

Celestia was left to arrive at the spacecraft from whatever direction it chose, which was frequently from inside the Earth. Where the observer starts is now worked out rather than left to chance: ten thousand kilometres from the spacecraft, along the line out from the centre of the Earth, so that looking at the spacecraft means looking on past it to the Earth below. It then follows the spacecraft as it did before, and can be flown by hand from there.

That has to be worked out rather than declared, so the startup script is now LUA rather than the older script language.

It also stopped the script running at all. It was mounted into the home directory, which is itself mounted from the host, and a file mounted inside another mount is hidden by it: what Celestia opened was the empty file Docker had left behind as the mountpoint. The startup script and the LUA hook are therefore mounted somewhere of their own, outside the home directory. The hook had been empty on the same account since the home directory started being kept, which nothing noticed because the module it makes findable is also answered for from inside the image.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Notice when Celestia has gone, instead of waiting to be told

Closing Celestia and opening it again left the spacecraft parked over the north pole for half a minute. Nothing was wrong with either end: the simulator simply had not noticed that the first one had gone. It asked whether an answer had arrived yet, slept a while, and asked again, so a closed connection was indistinguishable from a slow one and took two fifteen second timeouts to give up on. Until it did, the port was not open, the new Celestia could not get in, and it drew the spacecraft at the position it uses when it has heard nothing.

The answer is now waited for by reading it. A reader returns what it was given the moment it arrives, and returns nothing at all the moment the other end closes, so a Celestia that has been shut is noticed at once. The socket carries the timeout, so a client that has merely gone quiet is still given the fifteen seconds it always had, and is still sent the message a second time before being given up on. The handshake is read the same way, so a client that arrives and dies before answering does not hold the port either.

Measured on the way in and the way out: the port was free again a second after the client closed, where it had been half a minute.

The sleep that the old wait was built on was also the shortest time that could pass between two messages, whatever rate the visualisation data was produced at. It is gone, and with it that floor.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Celestia: fetch NASA Earth textures when building the image

The textures Celestia ships are small. The night side is forty kilobytes, and that is the half of every orbit the spacecraft is flying over the dark side of the Earth, so it is the one most on show.

The day and night maps are now fetched from NASA Earth Observatory while the image is built: Blue Marble: Next Generation at 5400x2700, and Earth at Night at 13500x6750. They are downloaded rather than committed because this repository has no business carrying tens of megabytes of imagery.

The build stops if anything is wrong with either file. A texture that quietly failed to arrive would leave an image that looks subtly worse with nothing to say why, so the download is checked three ways: the transfer itself, a minimum size, and the JPEG magic bytes. The last of these is not redundant. A server that answers a dead link with an error page, or a captive portal, returns a perfectly healthy response of a plausible size that is not an image, and only the magic bytes catch it.

Both files are within GL_MAX_TEXTURE_SIZE, 16384 on the hardware this was built for. The 21600 wide Blue Marble is deliberately not used: it cannot be loaded as a single texture.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Celestia: fetch a better cloud map when building the image

The cloud map Celestia ships is 594 kilobytes, and clouds are the part of the Earth nearest the camera, so it is the layer where the softness shows most once the day and night maps are sharp.

This is John van Vliet’s add-on from the Celestia Motherlode, built from NASA’s Blue Marble cloud_combined_8192.tif. Like the textures, it is fetched while the image is built rather than committed: seventy-eight megabytes has no place in this repository. He states no licence terms, so both he and NASA are credited in the Dockerfile and the README, which is as far as it can be taken.

It is a virtual texture, a pyramid of 512 pixel tiles reaching 8192x4096. The tiling is not needed here, since that is inside GL_MAX_TEXTURE_SIZE, but it is how the add-on was published in 2010, when cards could not hold a texture that large, and there is nothing to gain by unpicking it.

It brings its own .ssc, which reopens the Earth with Modify and sets CloudSpeed to 0. That is the reason solarsys.ssc is left untouched, and it matters: at Celestia’s default speed of 65 a cloud map this detailed is dragged into white stripes around the globe. It also raises CloudHeight from 7 to 20, which is the author’s choice and visible up close.

The archive is checked the same way the textures are, with the checks an archive allows: the transfer, a minimum size, the ZIP magic bytes, a successful unpack, the expected directory inside it, both catalogue files, and exactly 128 tiles at the top level. The build stops on any of them.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Celestia: call the spacecraft OPS-SAT on the wire too

The mission was renamed to OPS-SAT, but the rename stopped at the mission name. It mapped internally to a spacecraft id of OPSSAT, and that is what went out in the first field of every message, so the protocol still used the old spelling while everything a person sees used the new one.

Nothing has to change at the other end. The client reads the names and the values out of a message and never looks at the ids, which is why this went unnoticed. The stub server is renamed with it so that it keeps speaking exactly what the real one speaks; it would otherwise be the only place the old spelling survived, which is the sort of difference that costs an afternoon later.

Checked against a running stub: the message now opens

DATASTARTDATA_START PROTOCOLVERSION1.1PROTOCOL_VERSION_1.1 OPS-SAT OPS-SAT … // …

The Java identifiers keep their spelling, OPSSAT_TLE_LINE1 and CameraOPSSATAdapter among them. Those are names in code, not the protocol.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Celestia: stop drawing the ground station coverage domes

Each ground station is drawn as its telemetry visibility mask: the volume it can see out to 600 km, a dome sitting over the site. Drawn Emissive, so it glows flat pink whatever the lighting, and eight of them clutter the Earth without saying much.

They are switched off rather than removed, with a line above each saying what they are and how to bring them back. Turning one on is a one word edit, and the extras directory is mounted into the container, so it does not even need a rebuild.

This does leave the stations with nothing to draw, the mask being their only geometry. They remain as objects and can still be selected by name. Marking them without the dome would need a small model, which this set does not have.

Worth knowing before anyone turns them back on: darmstadt.ssc names kir-tm-mask-600km.3ds, which is Kiruna’s. There is no Darmstadt mask in the set, so Germany would be covered by a Kiruna shaped volume.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Split the simulator into two missions, lite and orekit

mission/simulator held two spacecraft simulators. One propagates the orbit and the attitude with Orekit; the other works the orbit out analytically and carries no library at all. Which one ran was decided at runtime, by a line in a configuration file, so the difference between them was invisible from the outside and neither could be built without the other’s weight.

They are now two missions at the top of the tree, beside the barebone one:

nmf-mission-simulator-lite cubesat-gps-simulator nmf-mission-simulator-orekit everything else, celestia included

mission/ is gone, simulator having been its only content.

This is a move. Nothing was rewritten, and git records 174 renames, 165 of them byte for byte. What changed beyond the paths is small and mechanical: the modules sit one level higher, so every relativePath loses a ../, and the groupId becomes int.esa.nmf, which retires int.esa.nmf.simulator. Artifact ids are untouched, so the playground’s dependency on nanosat-mo-supervisor-sim needed nothing but the new groupId.

The orekit mission depends on the lite one, since the analytic orbit is still the fallback when the propagator is switched off. That is left as it is for now; removing it is a code change and belongs in its own commit.

Also fixed, having been made wrong or found wrong by the move:

  • The gitignore kept exceptions naming opssat-spacecraft-simulator, a directory renamed to cubesat- long ago. They had matched nothing for years. They are dropped rather than repaired: the files they name hold machine local state, such as which port Celestia listens on, and reviving the exception would have started committing it.
  • platformsim.properties was tracked in one module and gitignored in the two others that produce it. It is written by the simulator at runtime and the committed copy held one developer’s absolute paths into a directory that no longer exists. Deleted, and now ignored like its siblings.
  • The orekit README pointed at cubesat-gps-simulator, which has moved, and at nanosat-mo-supervisor, which never existed under that name. It also said nothing about orekit-resources, image-debayer or celestia.
  • Both mission READMEs opened with a badge whose image was never defined anywhere in the repository, so it only ever rendered as broken.

Verified by a full build, by the simulator tests, and by starting the Supervisor: Orekit loads, the Directory service publishes its URI, the Celestia server listens, and nothing is logged above INFO.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Delete image-debayer, and debayer in the simulator instead

The module was a library for processing OPS-SAT camera images, and the OPS-SAT mission is over. It is not framework code, nobody runs what it was written for, and git remembers it if it is ever wanted again.

Its one caller was the simulated Camera service, which needs a picture whenever anything other than RAW is asked for. That is now done by a debayer method in ImageLoader, beside convertToBayerPattern, which it undoes. Five files and a dependency on net.imagej:ij go, and about fifty lines arrive. It also closes a licensing gap: three of the five files were modified ImageJ classes carrying no licence header at all, inside a module whose POM declared ESA-PL.

Two kinds of frame reach the debayer and they are not stored alike. A camera puts its reading low in the sixteen bit word; a frame made out of an ordinary picture by convertToBayerPattern puts eight bits high in the word with the lower byte at zero. The reading is therefore taken whole and brought down to eight bits by however much the brightest reading in the frame needs, which suits both. Reading only the upper byte suits the second and ruins the first, which is what the first version of this did: the one real camera frame in the tree came back very nearly black. There is a test for that now, and it fails if the byte is read that way again.

The pictures are not worse for the change. Against the original photograph, through the simulator’s own convert-and-undo path, the mean error per channel is 2.19 where the deleted library scored 3.31; the library was also stretching contrast, which is why its output was brighter and why it turned a nearly flat test frame into vivid colour. Nothing here stretches anything.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Cut the simulator loose from the analytic orbit, and from the flag

The simulator carried two ways of working out where the spacecraft was: Orekit, and an analytic orbit from cubesat-gps-simulator, chosen at runtime by “orekit=” in the header file. Only one of them is an implementation of anything. The two were hooked together long ago and should not have been.

The dependency and the analytic path go, and with them the flag. It was consulted in nineteen places, and eight of those were in the ADCS handler, every one shaped like this:

if (node.simulatorHeader.isUseOrekitPropagator()) {
    node.orekitCore.changeAttitude(...);
}

with no else. Turn the propagator off and every attitude command was accepted and quietly dropped. The same went for the telemetry: position only, no quaternion, no magnetic field, no sun vector, and Celestia unable to work at all. A simulator that answers every question with nothing while looking perfectly healthy is worse than one that refuses to start, so when Orekit fails to initialise it now throws instead of switching itself off and carrying on.

“orekit=” is no longer written into the header file and no longer read from it. Old files still carrying it are ignored rather than rejected: the parser’s switch has no default.

Two things follow from the flag going. getSpacecraftState has no invalid state left to report, so it throws nothing, and the IOException that had been threaded through it and out into NMEAFormatter goes with it; that file is back to what it was. And the OPSSAT_TLE constants were only ever the fallback’s answer, so they go too.

Also, while in the file: the logging in SimulatorNode went through Logger.getLogger in four places and this.logger in twenty-nine. The simulator’s own logger is the one that reaches its log, so it is now used throughout, and two calls that logged a null message now say what failed.

Verified by starting the Supervisor: Orekit up, Directory service published, Celestia server listening, and nothing logged above INFO.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Gather the simulator’s file names into one place

The simulator keeps five files beside itself and a directory under the user’s home, and their names were spelled out wherever they happened to be needed: five times in SimulatorNode, once more in ESASimulator, with the directory kept separately again. Names that have to agree, written out six times.

They are now in SimulatorFiles, and nowhere else. The strings themselves are unchanged, so the simulator writes and reads exactly what it did before; this only moves where they are written down.

The reason for doing it is the rename that follows. These files are named after OPS-SAT, and the simulator is meant for any CubeSat now. Renaming six scattered literals is how a simulator ends up writing one file and reading another, which does not look like a typo from the outside: it looks like the settings have been lost.

Verified by starting the Supervisor, which wrote the same three files with the same contents as before.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take OPS-SAT out of the simulator’s file and log names

The simulator is meant for any CubeSat, but everything it left on disk was named after the mission it was first written for. Its configuration files were _OPS-SAT-SIMULATOR-*.txt, its logs and data lived in ~/.ops-sat-simulator, and every line it logged began “OPS-SAT SoftSim:”.

They are now _SIMULATOR-*.txt, ~/.nmf-simulator, and “Orekit Sim:”.

Nothing on disk is abandoned. These files hold settings somebody chose, and the directory holds their logs and whatever raw camera and radio files they put there by hand; a rename that left them behind would have the simulator quietly writing fresh defaults over the top, which does not look like a rename from the outside, it looks like the settings have gone. So on startup anything still under the old names is moved across and the move is reported. Doing it twice is harmless, since there is then nothing to find, and it runs from both places the simulator can be entered through. The whole of it comes out again at v6.0. The gitignores name the old files as well as the new ones, which matters only between updating and running once.

The logs also stopped abbreviating. The nodes were called Sim and Cen, which is what named Cen_*.log, and there is nothing to be gained by saving three characters: they are Simulator and Central now, and the tail scripts follow.

The prefix on each line keeps “Sim” though, deliberately. The node that writes most of them is itself called Simulator, and “Orekit Simulator:Simulator” says it twice. There is a comment on the constant saying so.

Checked by planting a header carrying timeFactor=7 and a marked templates file under the old names and starting the Supervisor: both came across intact, so what is read afterwards is the file that was there and not a default. The home directory moved with its logs. Nothing is logged above INFO and no OPS-SAT prefix remains in the output.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Remove the SFTP browser, and the SSH client with it

The simulator carried com.jcraft:jsch, a full SSH client, so that its Swing client could browse for a camera image on a machine other than the one it was running on. Yannick Lavan added it in July 2019 for remote simulator configuration, pinned at 0.1.55, and it has not moved since. That is the last release of the original JCraft line, which stopped being maintained in 2018.

What the code did with it was narrow. SFTPBrowser performed exactly one operation in two hundred lines, channel.ls, so it could list directories and nothing else: no transfer, no exec, no rename, no delete. Credentials were typed into a dialog and never written anywhere, and the host key prompt asked a human rather than answering itself.

The dependency is another matter. jsch offers exec channels and port forwarding whatever our three classes chose to call, and it sat on the classpath of everything that depends on the simulator, the Supervisor included, where nothing ever used it. Six years of an unmaintained SSH stack shipped to serve a directory listing. The mission it was written for has ended and the local file chooser covers the case that remains, so it goes.

Two things change in the client. Setting a target connection no longer opens a credentials dialog; it refreshes the platform properties directly, which is what happened after a successful login anyway and never needed SSH. Browsing for a picture while the simulator is on another machine now says so and asks for the path to be typed, the picture having to be somewhere the simulator can reach.

Checked: no jcraft or jsch left in any source or POM, none on the dependency tree, no jar in the built filesystem, and the Supervisor starts with Orekit up and nothing logged above INFO.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Bind the quaternion listener to loopback

TCPServerReceiveOnly opened its socket with new ServerSocket(port), which takes the wildcard address and so listens on every interface the machine has. Whatever is sent to it is taken as the spacecraft’s attitude, and nothing is asked of the sender.

It is not started on its own. Simulator command 1204 opens it, on a port named in the command, which means anything already able to talk to the simulator could put an unauthenticated listener on the network and fly the spacecraft through it. That is a short step from the machine to the network, and there is no reason for it: the thing feeding attitude in is a tool running beside the simulator.

It now binds the loopback address, as MultiThreadedSocketServer already does, and says in the log which address it ended up on. Measured with the class’s own main: the socket comes up on 127.0.0.1 and a connection to this machine’s LAN address is refused.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop choosing platform adapters by class name from a file

The simulator’s platform provider read six class names out of platformsim.properties and brought each one up by name, so that individual adapters could be replaced without recompiling. It is gone, along with the reflection that served it: the provider now constructs the simulated adapters directly, and the file’s adapter keys are ignored.

This was not dead code when it was written. OPS-SAT ran this simulator on the spacecraft and on the flatsat, where some of the hardware is really there and the rest has to be pretended, and naming a class per service is how the camera answered for itself while everything else was simulated. The adapters belonged to the mission, which is why nothing in this repository ever named one, and why the configuration files here have always said platform.mode=sim.

That route is not being pursued again, and the mechanism is a poor one to leave lying about. A class name in a configuration file is resolved while the Supervisor starts, on a spacecraft, where nothing can be done about it going wrong; the compiler cannot see it, the build cannot check it, and whoever can write the file chooses what gets loaded and what runs in the static initialiser on the way. A mission wanting its own hardware behind the platform services writes its own provider and names that once, which is what nmf.platform.impl has always been for.

Two things also fixed by its going. A properties file without a platform.mode threw a NullPointerException that only IOException was catching, which took platform service startup with it; and the class shed a hundred lines.

The documentation said four different things about this and now says one. The reason is written down as well as the change, since the idea tends to come back.

Checked by starting the Supervisor with a file naming java.lang.Runtime and java.lang.ProcessBuilder as adapters: nothing is loaded, nothing is logged above INFO, and the simulated adapters come up as usual.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Start Celestia looking at the Earth the right way up

The observer was given (0, 1, 0) as its up vector, which inside Celestia is the pole of the ecliptic and not of the Earth: the two are the obliquity apart. That left the Earth leaning by 23.4 degrees, and by any angle at all when the line of sight came near the ecliptic pole, where the up vector is nearly the direction being looked along and what is left of it across the view points wherever rounding sends it.

The Earth’s axis is used instead, and the line of sight is kept 25 degrees clear of it so that the same thing cannot happen about that one.

Why it was intermittent: until the first sample arrives the spacecraft sits at its placeholder, which was directly over the north pole, and the startup script usually runs before then. That is the degenerate case exactly, and follow then carried the orientation over to the real position once the simulator answered. The placeholder is moved to the equator.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Start Celestia closer in, at five thousand kilometres

Ten thousand left the spacecraft a speck against the Earth.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop the camera launching a program when it takes a picture

Simulator command 1204 accepted “cameraScript:

It came in with the simulator itself, in the 2016 import of Cezar Suteu’s sources, and 1204 is described there as “generic for loading restricted subsystems”. That fits what the simulator was for: it ran on the spacecraft and on the flatsat, where a simulated picture had to poke a real camera. It is the same idea as the adapter class names in platformsim.properties, from the same time and for the same reason, and it goes the same way.

1204 now accepts only “quaternionServer”, and anything else it is given still fails as an unrecognised command. Whether that one stays is a separate question: it feeds attitude in from an external tool, and it now listens on loopback only.

Checked: cameraScript and ProcessBuilder appear nowhere in the module, the string does not appear in the built jar, and the Supervisor starts with nothing logged above INFO.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Bind App’s own storage directory in bubblewrap isolation (#192)

AppsLauncherManagerBubblewrap.assembleCommand() bound the App’s work directory and log directory into the bwrap sandbox but never $HOME/.nmf-apps//, which AppStorage/OneInstanceLock (used by any standard App via NanoSatMOConnectorImpl.init()) needs to write its instance lock file. As a result no standard NMF App could start under bubblewrap isolation, failing with:

java.nio.file.FileSystemException: /home/nmf/.nmf-apps/hello-world: Read-only file system at esa.mo.nmf.AppStorage.mkDirAndSetPermissions(AppStorage.java:160) at esa.mo.nmf.OneInstanceLock.(OneInstanceLock.java:42) at esa.mo.nmf.nanosatmoconnector.NanoSatMOConnectorImpl.init(NanoSatMOConnectorImpl.java:105)

Bind only the App’s own .nmf-apps/ subdirectory (not the shared .nmf-apps parent, so no other tenant’s storage is exposed), creating it up front with the same rwxrwx— permissions AppStorage itself uses, since bwrap –bind requires the source to already exist.

Verified with the SDK’s hello-world-simple, blank, and periodic-alert examples: all three crash identically on unpatched v5.1 and start/run cleanly across repeated iterations with this fix.

Co-authored-by: mpiirs mpiirs@taltech.ee

  • Bind the App storage directory the App will actually use

The bind added in #192 named the directory from the work directory as given, while the App names it from the canonical path: NanoSatMOConnectorImpl.init() does new File(new File(“”).getCanonicalPath()).getName(), having been chdir’d there by bwrap, and AppStorage builds the path from that.

The two agree for an ordinary directory, which is why this was not seen. They part company when an App folder is a symbolic link to a differently named one, and then the directory bound is not the directory the App writes to, so it fails exactly as it did before anything was bound at all. Checked both ways against a symlinked App folder: hello-world -> hello-v2 bound .nmf-apps/hello-world while the App wrote to .nmf-apps/hello-v2.

The path is now worked out the same way the App works it out. If it cannot be resolved, nothing is bound and the log says why, rather than binding a path that does not exist and leaving bwrap to fail with a less clear message.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Let Celestia listen and the simulator dial in

Celestia is the long-running end: a simulator may come and go several times while it runs, and the reconnect logic sat on the wrong side of that. Inverting it also means more than one simulator can show up in the same Celestia later without Celestia being told where each of them lives, which is what a constellation needs.

The protocol and the handshake do not change. The simulator still greets and Celestia still answers; only which end opens the socket moved.

CelestiaIf dials with a bounded connect, so a Celestia that is not running costs a moment and a retry rather than a thread parked in connect. Where to dial is a new celestiaHost setting beside celestiaPort, defaulting to loopback. The Lua binds once and accepts without waiting, since a frame is being drawn; it binds all interfaces rather than loopback so that a bridge network with a published port would also work.

The two tools in celestia/tools mirrored the old direction and are inverted with it: stub-simulator.py now dials, tap-simulator.py now listens. The port check in run_Celestia.sh warned when nothing was listening; the interesting case now is something already holding the port.

Checked three ways: CelestiaIf against a stand-in Celestia, 40 of 40 samples; the two tools against each other; and the Lua module itself inside the container driven the way a ScriptedOrbit drives it, 68 distinct positions over 400 frames, having first logged four refused connections while it waited for the port.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Removed the commons-exec dependency

Commons Exec served two purposes, both now covered by the JDK. The project targets Java 11: waitFor(timeout) and destroyForcibly() are Java 8, onExit() is Java 9.

nmf-services-impl used only EnvironmentUtils, for two methods, replaced by ProcessEnvironment. ofThisProcess() returns System.getenv() directly rather than a copy, preserving the case-insensitive lookup Windows requires.

The picture-processor and python-script example apps used the executor. They now use ProcessBuilder, with both streams redirected to the log file and the timeout applied through onExit(). The environment is cleared before the single required variable is set, reproducing Runtime.exec with a non-null environment: two variables rather than the inherited seventy-one.

Also: the log file location is reported at process start; PYTHONUNBUFFERED is set so a terminated script retains its output; both durations are reported in seconds, the unit the action declares.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Package the example app scripts beside the jar, not inside it

python-script and picture-processor kept their scripts under src/main/resources, which Maven adds to the jar, while the nmf-package plugin also copies them next to it. Every file was therefore uplinked twice: 126 bytes for a script, 97 kB for the font, and correspondingly more for anything larger such as a model. The directories now sit at the application root, as edge-ai already packages ai-model and demo-tiles. The deployed layout is unchanged.

picture-processor could not run its script. It invoked “python”, which recent distributions do not provide, and named imageEditor.py relative to the application directory while the script is packaged in a directory of its own. The script also loads its font by a relative name, so the process is now started in that directory rather than each path being corrected separately. Both faults predate the removal of commons-exec.

Verified in the playground deployment: python-script completes with exit 0 and its five lines, and is terminated at the maximum duration with three of them retained; picture-processor completes with exit 0 and writes a valid PNG.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Report an absent Celestia once, not on every attempt

The simulator dials Celestia every three seconds for as long as it runs, and Celestia is usually not there. Every refused connection was logged at SEVERE, which produced 70 identical lines in a three minute run of the Supervisor.

A refused connection is now reported once at INFO and suppressed until a connection succeeds, so a Celestia started later is still reported when it goes away again. Any other IOException remains SEVERE, and now carries the exception rather than its toString, so the stack trace survives.

The message names the host and port, which are configurable, and the retry interval is a constant rather than a literal in run().

Measured over a comparable Supervisor run with Celestia absent: 0 SEVERE lines and 1 INFO line, against 70 SEVERE before.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Upgrade to CCSDS MO services v14.2

The docs workflow no longer pins the mo-services-java checkout, which had to be bumped by hand on every MO release.

The Jars staged for the upgrade tests stay at MO 14.2’s predecessor, because they reconstruct the 5.0 baseline that the tests upgrade from, and 5.0 was released against 14.1. The version is now a named property.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Call Attribute directly for the conversion methods

HelperAttributes delegated javaType2Attribute and attribute2string to Attribute, which declares them with the same signatures, and no longer carries them as of mo-services-java 0ec25a42.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Format timestamps through Time and FineTime

HelperTime.time2readableString delegated to the instance methods and no longer exists as of mo-services-java 6fce197b.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Detect BusyBox when setting up linux-userspace isolation

The setup script took the user and group commands of a full Linux distribution. It now detects BusyBox and takes its commands instead, which is the check the NMF already makes when it creates the user of an App. The sudoers rule for addgroup is added only under BusyBox, where adding the group of an App is a command of its own.

The mission is deployed to both: the Raspberry Pi runs Debian and the Docker image runs Alpine, from one declared isolation mode.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take the Package Management backend of the Supervisor by default

The Supervisor constructs the backend of the packages directory when it is given none, which is what both missions were passing. They now call an init that takes the adapter and the Platform services alone.

Remove the workdirstorage property with it. It selected the home directory mode that the isolation modes replaced, and was written into the properties of every generated App while nothing read it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Log the output of an App at FINE

Every notification carrying the output of an App was logged as a warning, which is one line per chunk of output for as long as the App runs. The output itself is appended to the text area of the panel, so the console line adds nothing to it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Add the Raspberry Pi mission

The mission was kept in a repository of its own, built against the released NMF, and is now part of the framework it targets. The three modules take the parent, the groupId and the version of the framework, and are named after the mission: rpi-supervisor, rpi-platform-impl and rpi-space-filesystem.

The sources are relicensed from MIT to the ESA Public License of the rest of the framework, and the MIT licence files are removed with them.

The Constellation Management Tool, the ground proxy and the Docker setup stay where they are: they serve a constellation of segments rather than this mission, and are moved separately.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Add the Space File System of the Orekit simulator mission

The mission had no module to generate its Space File System, so there was nothing to build an image of. It follows the barebone one: the same three profiles to generate the filesystem, to upload it to a remote machine, and to build a Docker image of the Supervisor.

The orbit of the simulated spacecraft is taken from the environment, so that a constellation of containers is a constellation of orbits. The configuration it is written into moves here from the Raspberry Pi mission, which has real hardware and never read it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Add the files of a mission through the filesystem generator

The generator could always add a file of its own, but the goal exposed no way to ask for one, so the Orekit mission was laying its orbital dynamics configuration over the generated filesystem with the resources plugin. That needed the two to be declared in the right order in every profile. The goal takes the files instead, and adds them after the bootloader.

Remove the libs parameter with it: it was declared and documented, and never read.

The configuration is renamed to the name the simulator reads today. It was still the one from when the simulator was only OPS-SAT’s, and only reached the simulator because it renames what it finds under the old name.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Add the Supervisor and the Space File System of the lite simulator mission

The mission was the simulator alone, with nothing to run it, so it could not be deployed or built into an image. It gains a Supervisor, the Platform services of the simulator, and the module that generates its Space File System, which follows the two missions that already have one.

The Platform services are the GPS service alone. The simulator works the orbit out analytically rather than propagating it, so it knows where the spacecraft is and nothing else; the services that would need an attitude or a magnetic field report that they are not supported.

The orbit is taken from the environment, under the names the Orekit mission takes it under, so that a constellation can be built out of either image. That mission is given the elements in a file written before it starts; this one has no such file and reads them itself.

The simulator also works the position and the velocity out in the frame that turns with the Earth, which is the frame a receiver reports in, so that the GPS service can be given the two together. It had only the inertial velocity, and the pair would have disagreed by the turning of the Earth.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Add the Swarms mission

The Constellation Management Tool and the ground proxy were kept in the repository of the Raspberry Pi mission, which is where the constellation work started. They serve a constellation of segments rather than that mission, so they become one of their own.

The proxy is renamed after what it does: nothing in it was of the Raspberry Pi, and it already reaches a list of providers rather than one, which is what makes it the point the segments of a constellation are reached through.

The sources are relicensed to the ESA Public License of the rest of the framework. Eleven of them already carried it, six carried no licence at all, and the pom of the proxy still named the repository it came from.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Choose the image a segment of the constellation runs

The tool ran every segment from one image, named in the code, and it was the image of the Raspberry Pi mission. The segments are now created from an image chosen when the constellation is added, out of the missions that build one: the lite simulator, the Orekit simulator and the barebone mission. They take the orbit the same way, so a constellation can mix them.

The Raspberry Pi mission is not offered: it is an implementation for the hardware of a real spacecraft rather than a segment to simulate.

The container tool is chosen by a factory, as the note in its place asked for. The Kubernetes one reports that it is not written yet, rather than doing nothing at all and leaving a constellation silently empty.

The two windows are sized to what they hold. They carried a height that the row added to them no longer fitted in, which left the button off the bottom.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Put the node of a spacecraft in the domain of a fleet

The domain is built out of the organization, the mission and the App, and the units of a fleet share the first two, so a mission of more than one spacecraft addressed all of them identically. A mission that says it flies more than one now carries the node of this one between the mission and the App, which keeps its units together and leaves the App where it was.

A mission of a single spacecraft is left as it is: there is nothing to tell apart, and a level added to its domain would move every object already in its archive.

The domain is written as one string and split on the dot, so the node is left out, with a warning, where a value holds one rather than the domain quietly gaining a level.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Give each spacecraft of a fleet a node number and an address

A constellation runs one image many times, so its segments shared a domain and could not be told apart. Each container is now told which spacecraft it is through MISSION_FLEET and SPACECRAFT_NODE, which MissionConfiguration reads from the environment in preference to the mission.properties baked into the image.

The tool tracks the highest node it has issued rather than the size of the constellation, so that a node is not issued twice after segments have been removed.

Segments run on a network of their own, on which the address of a node carries its number: node n is at 172.28.<n/256>.<n%256>. The gateway is placed at the top of the subnet, which leaves the first address free for the first node. A segment is therefore addressable from its number alone, without the container being interrogated for it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • End the session when the window of the tool is closed

The window disposed only itself, so a second window of the tool, or a thread that a connection to a segment had left running, kept the tool alive after its window was closed. The segments of the constellation stayed up with it, because the shutdown hooks that remove them never ran.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Name the simulator images by what they are

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Follow the output of the selected segment

The pane was written only when the selection changed, so it showed what the segment had said at that moment and never changed again; the segment had to be deselected and selected again to see anything new.

The output is now read again every two seconds, off the event thread because the reading starts a process and waits for it. The pane follows the output while its end is shown, and keeps the place within it once it has been scrolled back.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say why the AI adapter was not started, without a stack trace

The adapter needs python3, which the image of a simulated segment does not carry, so it failed on every start of the simulator and was reported at INFO with the exception behind it: a trace for a condition that is expected there and says nothing unexpected.

Python is now looked for before the adapter is built, and its absence is reported as the condition it is. Anything else that stops the adapter is still reported with the exception, being unexpected.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Draw the spacecraft nearer its size, and start nearer to it

The model is in metres and correctly built, the body being 0.3 m and the deployed panels 0.7 m across, but it was drawn at 1500 times that: 1050 km across, a twelfth of the Earth beside it. It is now drawn at 300, which is 210 km, with the pointing vectors and the radii brought down by the same fifth so that the drawing keeps its proportions.

The observer starts fifteen hundred kilometres away rather than five thousand, which is about a third of the width of the image rather than a tenth of it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Raise a constellation from the command line

The tool only ever opened a window, so a constellation could not be raised on a machine without a display, nor from a script. Arguments now ask for one directly, and no window is involved in answering them.

The window is reached through a listener rather than held by the tool itself, and the dialogs move to the windows that ask for the segments: what the tool does is now free of Swing, which is what lets the same code serve a workstation and a headless machine.

The constellation lives as long as the command does. The segments are already removed by the shutdown hook each of them registers, so the command holds until it is interrupted rather than leaving containers behind that nobody is watching.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Name the ways a simulation is created after what they are

“Basic” and “advanced” said which of the two was the plainer, not what either one does. What separates them is where the segments come from: a number of them, or a file that gives each its own orbit. The menu says so, and the windows they open no longer contradict it.

The class and the form of the second follow the name it now carries, and the method behind it is named after the orbits it is given rather than after the window that reads them from a file.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Tell a segment of a constellation where Celestia is

The simulator dials Celestia, and the address it dials comes from the header file written into the image at build time. It says loopback, which is right when the two share a network and reaches nothing but the container when they do not, as for a segment of a constellation: the visualisation was therefore unavailable to one.

The environment now has the last word on where Celestia is, the way it already does on which spacecraft of a fleet a segment is, and where it says nothing the file stands as it did. The constellation tells each segment the gateway of its network, which is the machine holding it and so the machine Celestia runs on.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Give each segment of a constellation its own orbit

A constellation raised from the command line flew one orbit many times over: every segment had the orbit its image was built with. Orbits of their own were only to be had from the window, which a machine without a display does not have, and which cannot be scripted.

The file the window reads is now read by the command line as well, from one place rather than two, so that a file describes the same constellation whichever is given it. Reading it in one place is also what lets it be read properly: the window took the seventh field of a line and said “names must be unique” whatever had gone wrong, where the file is now answered for line by line and told what is the matter.

The segments of a file are named as the segments of a number already were. A row named leader raised a container called plainly that, which said nothing of what it belonged to among everything else a machine runs.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Tell each spacecraft of a fleet which one it is

The designation of a spacecraft is written when its filesystem is generated, so every spacecraft built from one image carried the same: the segments of a constellation each said they were node 1 of a mission of one spacecraft, whatever the tool had told them when it started them. Only the runtime knew otherwise, having read the environment; anything else reading the file was misled.

A script beside the bootloader now writes what the environment says into the file, so that what it says is what the spacecraft is. A spacecraft told nothing keeps the designation it was built with, which leaves a mission of one spacecraft exactly as it was.

The name of an individual spacecraft joins the fleet and the node in the environment, and the constellation gives each segment the name it was raised under: a segment is a container as well as a spacecraft, and the two are named for different readers.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Hold one constellation at a time on a machine

The segments of a constellation are numbered from one and are addressed by their number, so a second constellation raised beside the first asked Docker for addresses the first already had. Docker refused them, and the tool went on to announce a constellation that was not there, at a URI built from the words “invalid IP”.

Neither the command line nor the window now starts one where segments are already there. What is refused is refused before anything is run, so nothing is half raised and nothing has to be cleaned up afterwards.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take a refusal from Docker for a refusal

Docker says what went wrong on the error stream and then exits non-zero, and the tool read neither: it looked at the output stream alone, and only when that was empty at the other, inside a try whose catch was empty. What was left was a search for three phrases it knew, so every other failure passed for a success. A segment that Docker had refused to start was announced at an address it never had.

A command now answers with both streams together and is an error when it ends non-zero. The three phrases become advice about what to do rather than the only failures that exist, and a segment that is already gone still passes for one that has been dealt with: a constellation clearing itself and the shutdown hook of a segment both reach for the same container.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Produce a position for Celestia every time round the loop

The visualisation was fed every 200 ms by a loop that comes round every 100, and the thread that sends the positions waited 150 ms at a time for one to appear. Three periods, none of them a multiple of the others: a position was produced every other loop, and one in three of those waited a further wait before it was sent. What arrived came at 151 ms, and one gap in twenty at 451.

Both periods are now the period of the loop, so a position is produced every time round and sent as it appears: 101 ms, and the longest gap a third shorter. The comment that said a faster rate bought nothing was reasoning from the 150 ms wait, which was the thing in the way.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Turn the spacecraft in twenty seconds rather than thirty

A commanded attitude took half a minute to arrive at, which is a long while to watch a picture for before being sure anything is happening at all. The turn itself is what the time buys, and twenty seconds still buys a turn rather than a jump.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say what the spacecraft is doing in the corner of Celestia

The picture showed where the spacecraft was and which way it faced, and nothing of what it had been told to do. A commanded turn was therefore only to be recognised by the spacecraft beginning to move, which is several seconds after the mode has changed: long enough to wonder whether the command had arrived at all.

The mode was already being sent, in the INFO the simulator writes of itself, and nothing read it. It is now shown, with the moment of the last position beneath it: a picture that has stopped and one being fed the same position over and over look alike, and the clock tells them apart.

The corner is the left-hand one. Celestia lays a printed message out rightwards from where it is put, so from the right the text hangs off the edge unless the offset is made to match its width, and the offsets are not pixels but something like sixteen of them each.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Write the moment of a sample as a moment can be read

The date went on the wire as its fields were: a two-digit year’s worth of slashes, a day that lost its leading zero, a second that stopped at the second. Ten samples are produced within a second, so ten of them in a row said the same thing, and nothing said which clock any of it was kept by.

It is now ISO 8601, to the millisecond, in UTC and saying so. The date is joined to the time by a T because the message is read as a list of words and a space would split the moment in two.

On screen that T becomes a space again and the Z is left off: there is one clock in the picture, and it is easier on the eye.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Let the CLI pass an action a length of time

An action that takes a Duration could not be triggered from the command line: every number was read as a Long, and the provider was handed one where it asked for a length of time. It failed where the action is carried out rather than where it is submitted, so the CLI reported the submission as successful and the action quietly did nothing. That is how adcs.mode.nadir-pointing behaved.

A number with an s after it is now read as a Duration, which leaves every other argument as it was.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Ask for the test runner where the build will fetch it

The testbeds named a version of the test runner that no build of the framework asks for, and the testbeds are not part of that build. The cache a testbed job is given is the one the framework build saved, and what the framework never fetched was never in it: the runner was downloaded afresh on every testbed run, and a moment’s trouble at Maven Central failed the run before a single test had been reached.

The version is now named once, where every module of the repository can see it, so the framework build fetches it and the testbeds find it waiting. It also leaves the repository running its tests under one version of the runner rather than two.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Fly an orbit, or the default one, but never a row of zeros

A spacecraft told no orbit was given six zeros, and six zeros parsed as cleanly as any other six numbers. A semi-major axis of zero is no orbit: the propagator answered every question about where the spacecraft was with NaN, the visualisation left it hanging wherever it had last been drawn, and every service went on reporting itself healthy. That is what a constellation raised by count did, its segments being told the number of spacecraft and nothing about where they fly.

The image now fills in the orbit of OPS-SAT where it filled in zeros, and the simulator no longer flies what is not an orbit: elements that are missing, not six, not numbers, within the Earth, or of an eccentricity a spacecraft does not stay in are answered with the default orbit and a warning saying which of those it was.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Send the visualisation where the spacecraft is, not where it was

One position for the visualisation is produced almost every time round the simulator’s loop, and exactly one thing leaves the outbound queue per turn. More went in than came out: the picture fell a second behind for every five seconds the simulator ran. Fresh after a restart, half a minute behind after three minutes, which is why a commanded turn seemed to take ten seconds to appear on a view that was otherwise moving perfectly smoothly, and why the wait was a different length every time.

A position that is still waiting when the next one is made can never catch up, so it is dropped: the queue now carries the newest and nothing older.

The lateness could not be seen because each sample carried the simulator’s own Date by reference, and the simulator moves that one Date on every turn. Whatever had happened in between, a sample was written out saying it was of the instant it was written: one that had waited fifteen seconds arrived looking as new as one just made. Each sample now carries a copy, and says when it was really made.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Draw the spacecraft between the positions, not at them

Positions arrive about ten times a second and frames are drawn over a hundred: the spacecraft was drawn where the last position put it, so it stood still for eleven frames and jumped on the twelfth.

It is now drawn between two positions that are both already in hand, along the curve that passes through them and leaves each at the velocity the simulator gave it, which is the arc of the orbit rather than the chord across it. The attitude is taken the short way round between the same two. The positions kept for this are spaced half a second apart: an even interval draws a steadier curve than every position at an uneven one, and the ones passed over are on the curve anyway.

The moment being drawn is a clock of its own, steered towards where the positions say it should be rather than set from them, because setting it from them steps it backwards whenever one arrives late, and a step backwards is a spacecraft that twitches. Being two intervals behind is what makes the interval being drawn one whose ends both arrived before it began.

Celestia’s own clock is set again once it is up, rather than left at the time it started reading its catalogues: a minute behind showed in every attitude held against the Earth or the Sun.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Draw a constellation, not the first spacecraft to dial in

Celestia took one simulator and left the rest holding a connection nobody read: they completed the handshake, reported themselves connected, and sent into a socket that was never emptied. A constellation of four was a picture of whichever of them won the race.

Four of them are now drawn. Each keeps its own buffer, clock and reckoning of the lag, so one running late or going away disturbs nothing but its own spacecraft, and each drives an object of its own in the catalogue.

A simulator says which spacecraft it is when it greets Celestia, taking the name it was given when it was started. That is what a slot is bound to: a segment that comes back after a moment’s trouble on the link finds the spacecraft it had before rather than whichever slot happens to be free. A simulator that gives no name is still welcome and is labelled by its mission.

One too many is turned away rather than accepted and ignored, and told so. Both ends now say what is true once: the simulator no longer reports itself connected on the strength of the socket opening, before Celestia has agreed to anything.

The spacecraft are named for what they are rather than for the mission this file grew out of, and the names the simulators give are written in the corner beside them: Celestia reads the catalogue once, before any simulator has spoken, so the names in it can say only which is which.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Keep the Space Station and send the rest of the fleet home

Celestia is shipped with a handful of spacecraft, and several of them fly around the Earth: Friendship 7 and Gemini and Skylab, Hubble and Mir. They wander through a view of the spacecraft being simulated, labelled exactly as prominently as it is, and none of them has anything to do with the mission being flown.

The Station stays, because a cubesat drawn three hundred times larger than life is easier to judge against something whose size is known.

What is elsewhere in the solar system is left alone, being never in frame: Apollo 11 at the Moon, Mars Odyssey and MGS at Mars, Cassini and Galileo further out.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Call a spacecraft a cubesat where nothing else names it

Every spacecraft the simulator has flown since OPS-SAT ended has been announcing itself to the visualisation as OPS-SAT: the name was written into the code, and stood in wherever a spacecraft was not told its own. It went out twice over, in the greeting and against every parameter of every message.

The name a spacecraft is given when it is started still wins, so a segment of a constellation is itself. What is left when nothing names it is what it is: a cubesat.

The identifiers a message carries were a list of the missions known here by name, and a mission not among them was given none at all: the messages were then built for no spacecraft, so nothing was sent, and every log went on reporting a healthy link. They now follow the name the spacecraft goes by. NetSat, which flies four from the one simulator, keeps the four it had.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Answer for a whole constellation at one address

A consumer that wanted to reach a constellation had to be given the address of every spacecraft in it, one at a time. The Constellation Management Tool now raises one more container beside the segments: a Directory service that asks each node what it is and publishes them together, so a consumer connects to one address and finds them all.

What it publishes is the Supervisor of each node and nothing else. The apps of a node are the node’s own business, and are found through the node once it has been chosen; gathered together they would bury the spacecraft among their apps. The addresses published are the ones the nodes give, so a consumer that picks a node talks to that spacecraft directly: nothing is routed through here.

This is not the GroundMOProxy and does not replace it. That one serves a single spacecraft and does a great deal more, bridging protocols and mirroring the COM Archive. What is wanted here is the opposite: one thing that says what spacecraft there are.

A constellation that cannot be answered for is still a constellation, so a failure to raise the container is reported and passed over, naming the command that builds the image when it is the image that is missing.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Report the constellation through the logger

The tool said where the constellation was in two voices at once: the address of the Directory service was written out plainly while the line above it came through the logger, so one paragraph carried two formats. It is all said by the logger now, and the address with it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say why the constellation could not be answered for

The reason was passed to the logger but the message had nowhere to put it, so it was dropped: an operator was told that the Directory service had not started and nothing else. What went missing with it was the line that matters, naming the command that builds the image when it is the image that is not there.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Keep the COM Archive of the constellation Directory in memory

A provider that does not say where its COM Archive goes writes one into the directory it was started from, under a name that does not follow the version of the NMF. The Directory service of a constellation was doing exactly that, leaving a comArchive_v001.db wherever a constellation had been raised.

It has no use for an archive on disk: it rebuilds what it knows from the segments every time it starts, so anything kept from a previous run is at best a stale copy of what the nodes already report. It now asks for an in-memory archive, and its container stays free of state.

The documentation was wrong about the archive on two counts, and is corrected here: it named comArchive.db, and placed it in the working directory of the Supervisor. A provider has always written it to its NMF internal directory, under a name that follows the major version of the NMF: ~/.nmf-apps//nmf-internal/comArchive_v5.db

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say what this service does in plainer English

The comments of the constellation Directory service were written in a mannered style that got in the way of reading them: inversions such as “has otherwise to be given”, and closing lines that restated a point rather than adding to it, as in “What is wanted here is the opposite: one thing that says what spacecraft there are.”

They now say the same things directly. One method is renamed with them, askThePending() to askPendingNodes(), which had the same problem.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Write out each segment as it is raised

Raising a segment costs about 200 ms of Docker, and the segments are raised one after another, so a constellation of 40 nodes took nearly nine seconds. Nothing was written out until the last of them was up, which left the command looking like it had hung.

Each segment is now reported as it arrives. The tool tells its listener about each one through a new ConstellationListener.segmentRaised(), and the command line writes the line for that segment straight away. The method is defaulted to do nothing, so the window is unaffected.

This does not make raising a constellation any faster. Starting the segments in parallel would, and is worth doing separately.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say how long raising the constellation took

Raising a constellation takes about 200 ms a segment, which is several seconds for a large one. The report of it said only that the segments had been added, so the time it had taken was left to be guessed at.

It is now said, to the millisecond, on both the path that raises a number of segments and the one that raises them from a CSV file. The decimal point is a dot whatever the machine is set to, so that a time reads the same wherever it is reported.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Report a Directory service that could not be removed

The shutdown hook wrote this failure to stderr while everything around it went through the logger, so the one message that reported a segment left behind was also the one told apart from the rest.

It is logged now, and says what to do about it: a removal that fails leaves the container running on the machine, so the message names the command that clears it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Log the faults of raising a constellation

Two failures were written to stderr while the rest of the command went through the logger: a constellation that could not be raised, and a segment whose address could not be read.

Both are faults rather than anything the caller mistyped, so both are logged now, the first as severe and the second as a warning: a segment without an address is left out of the Directory service, and the rest of the constellation still stands. The hint about the use of Docker goes into the same record as the failure that prompts it, rather than following it as a line of its own.

What is left on stderr is what belongs there: the arguments the command could not make sense of, and the file that does not describe a constellation, each of them answered with the usage and an exit code to match.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Answer for the constellation from the tool, not the command

The Directory service that answers for a whole constellation was raised by the command line and by nothing else, so a constellation started from the window had no one address to be reached at.

It is the tool that raises it now, at the end of both the path that raises a number of segments and the one that raises them from a CSV file, so that the window and the command line alike get it. The command lost its own copy of that, and asks the tool for the address instead.

The container is also given a shutdown hook of its own, as every segment already has. Without one it outlived the window that raised it, and since a constellation is judged to be running by the containers named for it, that one container left behind was enough to have the next constellation refused.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Show the address of the constellation in the window

The one address a consumer connects to in order to reach the whole constellation was only written to the log, so it had to be fished out of the console of a tool that has a window.

It is shown in the window now, in a field below the segment count and the two managers, with a button beside it that puts it on the clipboard. The field is read-only but white, as a field that can be read from should look, and the button is enabled only while there is an address to copy.

The generated layout is checked in beside the form it comes from, so both carry the change.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop raising the constellation when the tool is interrupted

Interrupting the command while it raised a constellation crashed it and left a container behind:

Exception in thread "main" java.lang.IllegalStateException: Shutdown in progress
    at java.base/java.lang.Runtime.addShutdownHook(Runtime.java:250)
    at esa.mo.nmf.cmt.utils.NanoSatSimulator.<init>(NanoSatSimulator.java:92)

The hooks of a shutdown run on threads of their own while the thread that raises the constellation carries on, so the tool went on raising segments after it had been asked to end. The segment being made when the interrupt arrived could not add the hook that would have taken it down, and the one after it had its container made by a run() that came after its own hook had been and gone: a container nothing was left to remove. The address of a segment read while its container was being removed is where the ‘invalid IP’ in the report came from.

The tool now knows it has been asked to end, and the loops that raise the segments stop when it has. A segment raised in the moment the interrupt arrived is taken down by the loop itself, and the two places that add a hook late are no longer surprised to be refused one.

What became of the constellation is also said now. The hooks of the segments cannot say it, since they run alongside each other and none of them is last; this asks the container tool what is left and reports that, so whoever interrupted the command is told the segments are gone rather than left to wonder. It is written out rather than logged: the logger closes its handlers from a shutdown hook of its own, and records logged from another one are as likely to be thrown away as printed.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say which image the segments of a constellation run

Every segment of a constellation runs the same image, and which one it is decides what the spacecraft can do: the lite simulator works its orbit out analytically, the Orekit one propagates it and answers the rest of the Platform services. Which one was running had to be worked out from whether –image was passed and from what the default happens to be that week.

It is now named before anything is raised, by the name the container tool knows it by and by the name the window offers, so that the line reads the same whichever of the two is being matched against.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take an interrupted container tool for the interrupt it is

Interrupting the command while it raised a constellation reported a failure to raise it, with a stack trace, over an interrupt that had done nothing wrong:

SEVERE: The constellation could not be raised.
java.io.IOException: This command ended with 130: docker run ...

An interrupt from a terminal is delivered to every process of the foreground group, so the container tool being run at that moment dies of the same key that stopped this one, and ends with 130 as anything killed that way does. The command that ended is not a command that failed.

A segment that could not be raised while the tool is ending now stops the raising quietly, the failure is no longer logged as one, and the command line ends as it would have anyway rather than saying the constellation could not be raised. What became of the constellation is still said, as it was.

This was reached by interrupting the process group, as a terminal does, rather than the one process: interrupting the tool alone leaves the container tool it started untouched, and never showed this.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take out the null checks that could never be true

CodeQL reported a family of checks that can never fail: a value taken straight from new, a check already made a few lines above, a check made again inside the guard that implied it. They are gone.

Three of them stood in for a check that is worth making, and those are not merely deleted:

The Facebook example asked whether the client it had just made was null and said it was “connected” when it was not. Making a client neither connects nor tries the token; a token is only found to be wrong when something is posted. That is where it is answered now, and a post that is refused says so instead of throwing out of the data listener.

FastProviderURI asked whether a URI it had just made was null. What can be missing there is the id, not the URI: an id the index does not hold gave a URI of nothing and handed it back as good. The id is now looked up first and a missing one is said to be missing, as FastObjectType has always done.

ParameterManager asked whether the Conversion service was null, and it never is. The comment above it asked the right question all the same, and the answer is in ConversionServiceImpl rather than here.

The rest are as reported: six providers checked the same argument for null twice in a row, an adapter checked a source that the equals above it had already proved, a socket server checked a command inside a check of the same command, and a window checked an area that having a service already implied. Where a check was taken away because something else implies it, the implication is written down, so that the next reader is not left to work out why it is safe.

Two things found while reading around them are not fixed here: the socket server logs at Level.ALL in several other places, which is a threshold and not a level, so those lines are never printed; and ConversionServiceImpl holds a ParameterManager that nothing ever assigns, so a conversion with a condition would meet a null.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take out the instanceof checks that could never fail

Two more of the same family, both in the window that edits an MO element:

The Attribute check asked whether the object was an Attribute and not a list, and returned the same expression whichever way it went as the line after it. Whatever the object was, the answer was the same, so the check is gone and the line after it is the answer.

The Composite check asked whether the object was a Composite and not a list. A list cannot reach it: the check above it takes every list and returns. Only the part that asks about lists is gone, since the rest of that check still decides something.

The comment there said “Composite not list”, which described the part that has been taken away. It now says where the lists went instead, so that the check is not put back to match the comment.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Say there is no stream without asking the stream its name

Reading the method descriptions warned about a stream it could not read by putting the stream in the message. It reached that line only when the stream was null, so the warning was a call on nothing and would have thrown where it meant to explain:

"InputStream [" + fileName.toString() + "] could not be accessed!"

Nothing has met it, because the one caller looks for the resource and says so itself when it is missing, leaving the branch unreachable. It is one refactor away from being reached.

The check is now made first and returns, which flattens the rest of the method, and the parameter is called stream rather than fileName: it is an InputStream, and asking one for its name is what led to the message that could not be written. A stream has no name to give.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop calling finalize() to close a tab

Closing an Archive or ArchiveSync tab called finalize() twice: once on the adapter, once on the mouse listener itself. Neither class overrides it, so both calls reached Object.finalize(), which does nothing. The catch of Throwable around them could not catch anything either.

Taking only the calls away would have left a method called finalizeAdapter() with an empty body and a comment saying it releases the resources of the adapter, so it is gone with them, and so is the call to it. An adapter holds a table, a panel and a semaphore, none of which has to be given back, and the tab is already taken off by the remove above. Removing the tab is now the whole of closing it.

Object.finalize() has been deprecated for removal since Java 18, so these were also on their way to being a compile error rather than a warning.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Take out the containers that nothing ever read

CodeQL found collections that are made and filled and never looked at again. Three of them are gone:

The GPS provider built a BooleanList of the one value it was about to publish, beside an UpdateHeaderList it never put anything in, and then published the value itself. Publishing the value is right: the generated publisher takes a Boolean. Both lists are what is left of the older API that published lists of updates.

The simulator node made a LinkedList of interface files at startup and never put a file in it or read one out.

The window that edits the simulator header collected the time factor and the two dates into a list and then built its fields from the header instead, so the list and the three values that fed it are gone.

Left alone: the HashMap in ArchiveToJsonAdapter that CodeQL reports the same way. Nothing in Java reads it, but Gson does, by reflection, when the archive is dumped to JSON. Taking it out would quietly drop the object from every dump.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Delete from the Archive when the Delete button is pressed

The Delete button of the ArchiveSync tab took the entry off the table and left the object in the Archive. The call that would have deleted it was commented out, and the list of ids it was given was left behind, which is how CodeQL came to report a list that nothing reads.

The call is made now. It was commented out rather than fixed, and there were two ways it could not have worked:

The tab that is selected is not always a table. The Home tab is a panel of its own, and asking it for the objects it holds is a cast that throws. That was true of taking the entry off the table as well, so the button already threw there with the delete commented out.

A table with no row chosen gives back the row -1, and the list of objects is then asked for its -1st.

Both are answered before anything is deleted. The entry is taken off the table only once the Archive has let the object go: a delete that fails now leaves the row where it is and says so, rather than showing a table that no longer matches the Archive.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Put the commas back between the values of a dumped array

The separator in the array branch of dump() was written when i < 0, and i counts up from zero, so it was never written at all. An array of three values came out as [123], which is the same as one value of 123 and the same as [12, 3]: the dump could not be read back.

It is written between the values now, as it was meant to be:

int[]    -> [1,2,3]
String[] -> [a,b]

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Synchronize the override that empties the package table

SharedTablePanel guards its table with the monitor of the panel, and removeAllEntries is one of the four methods that hold it. The package table overrides that method and took the rows away without holding anything, so it could empty the table while another thread was reading a row out of it.

It holds the monitor now, as the method it overrides does. What the override leaves out is right: it does not remove from comObjects, and it has none to remove, being a table of packages rather than of COM objects. That is why addEntry of a COM object is refused here.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Close the files that were written and left open

Two streams were opened for writing and never closed.

The simulator wrote platformsim.properties through a stream it dropped. A properties file is written through a buffer, and a stream that is never closed is never flushed, so the file could be left short of what was stored into it or empty altogether, depending on how much there was to write and when the collector came round. It is closed now, and flushed by the closing.

The demo package downloader left the file it wrote open, and the channel it read from as well: that one holds the connection, so a download that failed part way through kept a socket as well as a file. Both are closed now, and only the transfer is inside the block, so the time reported for the download is still the time of the download.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Name the LOG file that could not be opened

The message was formatted with the path of the file and had nowhere to put it, so the path was dropped and the reader was told only that a LOG file could not be opened. The writer is then left as null and the tool carries on writing nothing, which is the moment to say which file it was.

No other format call in the repository is short of a placeholder.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Leave the AI inference script by the exit that is always there

The script gave up with exit() when no MYRIAD device was found. That one is put in place by the site module rather than the language, so it is not there under python -S, and the script would have raised a NameError at the very point where it was trying to say what was wrong.

It uses sys.exit() now, which is always there. No other Python in the repository calls the bare exit() or quit().

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Import the shared output of the e2e tests instead of inheriting it

Every end-to-end test extended NMFTest, a class of thirty-five lines holding a logger, two separators and a message, and nothing else: no setup, no teardown, no method to inherit. Extending it did nothing but bring four names into scope, and spent the one superclass a test has to give away on a class with nothing to give.

The four are imported statically now, each test taking the ones it uses, and the holder is a final class with no constructor to call.

It is called SharedOutput rather than NMFTest because it is no longer a test of anything, and because it must not be taken for one: Surefire runs whatever is called Test* or *Test, and only the abstract on the old class kept it from trying to run this. The reason is written in the class, so that the name is not tidied back into one of those.

Sixteen tests extended it, not the eleven that were reported.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Put back the space between two halves of a message

The two halves of the timeout message ran together as “The process is stuck…The adapter will kill the process!”. The same sentence in LinuxUsersGroups keeps the space, so this is a copy of it that lost one.

No other message in the repository is joined this way. The thirteen places where two literals meet without a space are all GPS sentences in HelperGPSTest, where the space would be part of the data.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop making Longs with a constructor

Six places made a Long by calling its constructor, which allocates one where the cached value would have done. They pass the number itself now, and the boxing that follows is the valueOf they were told to call.

A seventh was not reported and is not the same: it made a Long out of the text of a table cell, which is the constructor that parses rather than the one that boxes. That one is Long.valueOf of the String, which parses as it did and fails on bad text as it did.

These constructors have been deprecated since Java 9 and are marked for removal, so they were on their way to failing the build rather than merely being remarked upon. There is no longer a constructor call for any boxed type anywhere in the repository.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Stop copying Strings that were already Strings

A test made a copy of a literal to pass it as an object, and the Apps Launcher started an empty one with the constructor. Both allocate a String where one already exists: the literal, and the interned empty string.

The second was not reported, the rule being about String(String) rather than String(). It is the same waste and it is in the path that publishes the output of an App rather than in a test.

Every other call of that constructor in the repository makes a String out of bytes or characters, which is what it is for, and none of them is touched.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Tell apart the App looked for from the App looked at

The adapter that finds an App by name held the name it was looking for in a field called appName, and gave the name of each App it examined a local of the same name. The two were told apart by a this., and the comparison read this.appName.equals(appName).

Left off, that is appName.equals(appName): always true, compiles, and the adapter would answer with the first App in the archive whatever was asked for. The local is candidateName now, and the line says which is which without a prefix to notice.

No other method in the repository declares a local named after a field it also reaches through this.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Give the MC testbed a fork of its own and a time to give up

The MC Area testbed hung on JDK 11 and was killed by the fifteen minute limit on the job, which says that time ran out and nothing else: no test failed, no class was named, and the same testbed passed on 17, 21 and 25 in the same run and on every commit since.

It now runs each test class in a JVM of its own and gives a fork five minutes, which is what the COM testbed already does. The reason is the same: these tests stand up MAL providers and consumers on the RMI transport, whose state belongs to the process, and a fork shared between classes carries that state from one to the next. The COM testbed records DirectoryLookupTest hanging that way when it ran after the others.

Whether that is what hung here is not proven: it happens seldom, only on one JDK, and it did not happen again to be watched. What the timeout does settle is that the next one ends as a fork that timed out, with a dump naming the class that was in the middle of it, rather than as a job that was cancelled.

Nine tests across four classes, five seconds, so four JVMs instead of one cost nothing worth counting.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Move the attitude test to a testbed, out of the build

A full build of the framework took twenty-nine seconds, and four of them were one test: AttitudeSwitchingTest propagates an orbit through five commands of seventy-five seconds each, three hundred and seventy-five steps, to prove that every command changes the attitude and not only the first. That is an integration test, and the testbeds are where they go.

It has moved to testbed-e2e, which the root build does not build. A full build is twenty-five seconds now. It runs in CI as it did, the e2e job running the tests of that module without naming them, so there is no job to add and none of the four JDKs loses the test.

The other six tests of the simulator stay where they are. They cost between twenty-six and eighty milliseconds together, and whoever edits the simulator should still have them fail in front of them.

testbed-e2e is not the natural home for orbital mechanics. A testbed of its own would read better, but each testbed is four more CI jobs of a minute apiece to save four seconds, which is the wrong way round.

The simulator writes files into the directory it runs from, which the simulator’s own module ignores. The test took that habit with it, so the testbed ignores them too.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Make the mission package type one that can be made

The type existed in name: a package could call itself a mission, and the Package Manager knew what that meant, but nothing could produce one and the builder had no place to put the files of one. Built anyway, a mission package carried its Jars at the root of the archive, and installing it would have strewn them over the top of the filesystem.

What was already there is most of it. The runtime has had a place for a mission baseline all along: jars-mission holds a directory per version, the baseline names a mission-version beside the nmf-version, and the bootloader builds the classpath from the two of them, mission first, so one can be replaced while the other stays. The Package Manager decides what to refresh and what to refuse from the path of the files rather than the type of the package, so it already treated jars-mission as a baseline directory and already refused to write over the factory one.

So what is added is the making of such a package:

MetadataMission, and the branch in the builder that sends its files to jars-mission under the version of the mission.

A generate-mission-package goal, and an execution of it in the Orekit simulator mission, beside the one that generates the filesystem and after it. It packages the jars-mission directory that one produced rather than working the set out again: generate-filesystem sorts the Supervisor’s dependencies into framework and mission, and deriving that a second time is a second copy of the rule, free to drift from the first. This was tried the other way first, and a package of twenty Jars came out where twelve were wanted.

Two tests, for the two things that can be wrong. Where the files go is held by a unit test, which builds a package and looks inside it. What becomes of them is held in the end-to-end testbed, which installs one into a running Supervisor and holds that the Jars arrive in jars-mission under their own version, that the mission and framework that are running are both still there, and that the manifest the bootloader verifies against is written. That one builds its own package: there is no released mission baseline to upgrade from, this being the first version to carry them.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Let the parent say which version of the composites to use

The swarms proxy named the version of nmf-composites itself, where the parent already manages it. It was the only module of the forty-four that depend on it to do so: the one other place that names a version is the end-to-end testbed, which wants an older one on purpose.

Naming it twice means it can be changed once. Resolution is unaffected, both saying 5.1-SNAPSHOT today, so this is the second one going away.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Write down what changed in the 5.1 release

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Fix the Javadoc of the Python App and the constellation tool

The Python App documented a constructor parameter that went away with the commons-exec dependency. The constellation tool documented the container naming scheme with angle brackets, which Javadoc read as HTML, and its gui package comes from the IntelliJ GUI Designer, whose comments Javadoc will not accept, so that package is left out of the report.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Document the generated GUI classes instead of hiding them

Javadoc does not know the @noinspection tag of the IntelliJ GUI Designer, and it reads the angle brackets of the generated note as HTML. The tag is dropped and the brackets are escaped, so the gui package no longer has to be left out of the Javadoc report.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Bump version from 5.1-SNAPSHOT to 5.1 for the release

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Upgrade the benchmark package to a version the release cannot have

The manufactured upgrade package was versioned 5.1, and the test deletes that file before it looks up the benchmark package, to clear a fixture left by an earlier run. Once the project itself became 5.1, the file deleted was the real package, and the lookup found nothing.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

  • Attach the NMF core package without a classifier

The module attaches a single NMF Package, so the classifier only lengthened the name the package is installed and deployed under.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com


Co-authored-by: Claude Opus 4.8 noreply@anthropic.com Co-authored-by: mpiirstatebuilding 117380831+mpiirstatebuilding@users.noreply.github.com Co-authored-by: mpiirs mpiirs@taltech.ee

19小时前1129次提交

esa logo

NanoSat MO Framework

Maven Central Build Status

The NanoSat MO Framework is a software framework for small satellites based on CCSDS Mission Operations services.

It introduces the concept of apps in space that can be started and stopped from ground. Apps can retrieve data from the platform through a set of well-defined MO services. Many possibilities for extensions are available due to its modular and flexible design approach which is not limited to the space segment but extends down to ground by providing all the building blocks for a complete and free end-to-end solution.

A Software Development Kit (SDK) is available in order to facilitate the development with the NanoSat MO Framework.

Wikipedia page: https://en.wikipedia.org/wiki/NanoSat_MO_Framework

Documentation: https://nanosat-mo-framework.readthedocs.io/en/latest/

Interfaces of the services: https://dmarszk.github.io/MOWebViewer4NMF/

Research Work Dissertation: https://www.researchgate.net/publication/321825076

The research was carried by Graz University of Technology in partnership with the European Space Agency.

Release

The latest NMF SDK release tag can be downloaded in Releases. However, the currently recommended distribution channel is directly from Git repository.

Building Prerequisites

  1. Install Java SDK 11 (will work with higher SDKs but 11 is the recommended)
    sudo apt-get install openjdk-11-jdk
  2. Install Apache Maven
    sudo apt-get install maven

Building Instructions

  1. Clone this repository

    git clone https://github.com/esa/nanosat-mo-framework.git
  2. Build the cloned NanoSat MO Framework project:

    mvn clean install

Note that the errors about missing module descriptors during Javadoc generation are to be expected and non-breaking.

In order to produce independently runnable Java executables (JAR artifacts with dependencies - equivalent of statically linked executables), use mvn install -P assembly-with-dependencies

Getting Started

SDK and examples

More documentation about code examples, SDK packaging and usage is available under sdk directory.

Logging

This project uses the default Java logger (java.util.Logger) to generate log messages. The verbosity of these log messages can be changed by configuring the logging.properties file inside the Supervisor’s working directory.

Source Code

The source code of the NanoSat MO Framework can be found on GitHub.

Bugs Reporting

Bug Reports can be submitted on: Issues

Or directly in the respective source code repository.

License

The NanoSat MO Framework is licensed under:

European Space Agency Public License (ESA-PL) Weak Copyleft – v2.4.

关于

ESA 基于 CCSDS MO 服务标准的小卫星在轨应用框架,提供标准星地交互与应用部署能力。镜像收录自 https://github.com/esa/nanosat-mo-framework,未检测到 LICENSE 文件

147.8 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号