pymoos2 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymoos2-0.1.0/LICENSE +21 -0
- pymoos2-0.1.0/PKG-INFO +102 -0
- pymoos2-0.1.0/README.md +82 -0
- pymoos2-0.1.0/pymoos2/__init__.py +48 -0
- pymoos2-0.1.0/pymoos2/__main__.py +201 -0
- pymoos2-0.1.0/pymoos2/core/ACTable.py +45 -0
- pymoos2-0.1.0/pymoos2/core/AppCastingMOOSApp.py +177 -0
- pymoos2-0.1.0/pymoos2/core/MOOSApp.py +280 -0
- pymoos2-0.1.0/pymoos2/core/MOOSCommClient.py +236 -0
- pymoos2-0.1.0/pymoos2/core/MOOSCommPkt.py +36 -0
- pymoos2-0.1.0/pymoos2/core/MOOSMsg.py +144 -0
- pymoos2-0.1.0/pymoos2/core/MissionReader.py +107 -0
- pymoos2-0.1.0/pymoos2/core/__init__.py +0 -0
- pymoos2-0.1.0/pymoos2/core/constants.py +47 -0
- pymoos2-0.1.0/pymoos2/core/time.py +67 -0
- pymoos2-0.1.0/pymoos2/dtos/__init__.py +0 -0
- pymoos2-0.1.0/pymoos2/dtos/contacts/NodeRecord.py +150 -0
- pymoos2-0.1.0/pymoos2/dtos/contacts/__init__.py +0 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYArrow.py +38 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYCircle.py +38 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYCommsPulse.py +41 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYMarker.py +52 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYObject.py +99 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYOval.py +40 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYPoint.py +37 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYPolygon.py +48 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYRangePulse.py +44 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYSegList.py +60 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYTextBox.py +48 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYVector.py +41 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/XYWedge.py +37 -0
- pymoos2-0.1.0/pymoos2/dtos/geometry/__init__.py +13 -0
- pymoos2-0.1.0/pymoos2/templates/App.py.tmpl +115 -0
- pymoos2-0.1.0/pymoos2/templates/CMakeLists.txt.tmpl +21 -0
- pymoos2-0.1.0/pymoos2/templates/Main.py.tmpl +97 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/clean.sh +36 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch.sh +98 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch_shoreside.sh +130 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch_vehicle.sh +176 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_shoreside.moos +117 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_vehicle.bhv +56 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_vehicle.moos +175 -0
- pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/plugs.moos +59 -0
- pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/CMakeLists.txt +20 -0
- pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/NodeWatch.py +180 -0
- pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/__pycache__/NodeWatch.cpython-312.pyc +0 -0
- pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/__pycache__/pNodeWatch.cpython-312.pyc +0 -0
- pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/pNodeWatch.py +96 -0
- pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/CMakeLists.txt +20 -0
- pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/WptPulse.py +107 -0
- pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/__pycache__/WptPulse.cpython-312.pyc +0 -0
- pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/__pycache__/pWptPulse.cpython-312.pyc +0 -0
- pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/pWptPulse.py +96 -0
- pymoos2-0.1.0/pymoos2.egg-info/PKG-INFO +102 -0
- pymoos2-0.1.0/pymoos2.egg-info/SOURCES.txt +58 -0
- pymoos2-0.1.0/pymoos2.egg-info/dependency_links.txt +1 -0
- pymoos2-0.1.0/pymoos2.egg-info/entry_points.txt +2 -0
- pymoos2-0.1.0/pymoos2.egg-info/top_level.txt +1 -0
- pymoos2-0.1.0/pyproject.toml +37 -0
- pymoos2-0.1.0/setup.cfg +4 -0
pymoos2-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ray Turrisi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pymoos2-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pymoos2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python MOOS-IvP application framework
|
|
5
|
+
Author-email: Raymond Turrisi <rturrisi@mit.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/raymondturrisi/pymoos2
|
|
8
|
+
Project-URL: Repository, https://github.com/raymondturrisi/pymoos2
|
|
9
|
+
Keywords: moos,moos-ivp,robotics,auv,marine,middleware
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: Unix
|
|
16
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
|
|
21
|
+
# pymoos2
|
|
22
|
+
|
|
23
|
+
Pure-Python MOOS-IvP application framework. Write MOOS apps in Python with full AppCasting, wire protocol, and pAntler integration.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pymoos2
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Generate a new app:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd moos-ivp-myproject/src
|
|
37
|
+
python3 -m pymoos2 --GenAppCastingMOOSApp MyApp p "Your Name"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This creates `pMyApp/` with a ready-to-edit Python MOOS app that:
|
|
41
|
+
- Connects to MOOSDB over the native wire protocol
|
|
42
|
+
- Supports AppCasting (shows up in uMAC/pMarineViewer)
|
|
43
|
+
- Reads ProcessConfig from `.moos` files
|
|
44
|
+
- Installs to `bin/` via CMake alongside C++ apps
|
|
45
|
+
- Responds to `ktm` shutdown signals
|
|
46
|
+
- Handles time warp correctly
|
|
47
|
+
|
|
48
|
+
## Example Mission
|
|
49
|
+
|
|
50
|
+
Generate a complete working example with two apps and a mission:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd moos-ivp-myproject
|
|
54
|
+
python3 -m pymoos2 --example
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Writing an App
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pymoos2 import AppCastingMOOSApp, ACTable
|
|
61
|
+
|
|
62
|
+
class MyApp(AppCastingMOOSApp):
|
|
63
|
+
|
|
64
|
+
def on_startup(self):
|
|
65
|
+
self.radius = self.config("radius", default=50.0, type=float)
|
|
66
|
+
self.register("NAV_X")
|
|
67
|
+
self.register("NAV_Y")
|
|
68
|
+
|
|
69
|
+
def on_new_mail(self, mail):
|
|
70
|
+
for msg in mail:
|
|
71
|
+
if msg.name == "NAV_X":
|
|
72
|
+
self.nav_x = msg.double()
|
|
73
|
+
|
|
74
|
+
def iterate(self):
|
|
75
|
+
self.notify("MY_OUTPUT", self.nav_x * 2.0)
|
|
76
|
+
|
|
77
|
+
def build_report(self):
|
|
78
|
+
tab = ACTable(["Variable", "Value"])
|
|
79
|
+
tab.add("NAV_X", f"{self.nav_x:.2f}")
|
|
80
|
+
self.report(tab)
|
|
81
|
+
return True
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
- Pure Python — no C++ compilation, no pybind11
|
|
87
|
+
- Full MOOS wire protocol (binary TCP, same as C++ clients)
|
|
88
|
+
- AppCasting and RealmCasting support
|
|
89
|
+
- Time warp support (`MOOSTimeWarp`)
|
|
90
|
+
- `ktm` (kill the moos) UDP multicast listener
|
|
91
|
+
- Mission file parsing (ProcessConfig blocks, global params)
|
|
92
|
+
- Geometry DTOs for all VIEW_* types (XYPoint, XYPolygon, XYCircle, etc.)
|
|
93
|
+
- NodeRecord parsing
|
|
94
|
+
- `py_compile` syntax checking at build time
|
|
95
|
+
- Works with pAntler — shebang'd scripts in `bin/`
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Python >= 3.8
|
|
100
|
+
- MOOSDB running (from moos-ivp)
|
|
101
|
+
- No other dependencies for core functionality
|
|
102
|
+
- `flask` for web-based viewer apps (optional)
|
pymoos2-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pymoos2
|
|
2
|
+
|
|
3
|
+
Pure-Python MOOS-IvP application framework. Write MOOS apps in Python with full AppCasting, wire protocol, and pAntler integration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pymoos2
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Generate a new app:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd moos-ivp-myproject/src
|
|
17
|
+
python3 -m pymoos2 --GenAppCastingMOOSApp MyApp p "Your Name"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates `pMyApp/` with a ready-to-edit Python MOOS app that:
|
|
21
|
+
- Connects to MOOSDB over the native wire protocol
|
|
22
|
+
- Supports AppCasting (shows up in uMAC/pMarineViewer)
|
|
23
|
+
- Reads ProcessConfig from `.moos` files
|
|
24
|
+
- Installs to `bin/` via CMake alongside C++ apps
|
|
25
|
+
- Responds to `ktm` shutdown signals
|
|
26
|
+
- Handles time warp correctly
|
|
27
|
+
|
|
28
|
+
## Example Mission
|
|
29
|
+
|
|
30
|
+
Generate a complete working example with two apps and a mission:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd moos-ivp-myproject
|
|
34
|
+
python3 -m pymoos2 --example
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Writing an App
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pymoos2 import AppCastingMOOSApp, ACTable
|
|
41
|
+
|
|
42
|
+
class MyApp(AppCastingMOOSApp):
|
|
43
|
+
|
|
44
|
+
def on_startup(self):
|
|
45
|
+
self.radius = self.config("radius", default=50.0, type=float)
|
|
46
|
+
self.register("NAV_X")
|
|
47
|
+
self.register("NAV_Y")
|
|
48
|
+
|
|
49
|
+
def on_new_mail(self, mail):
|
|
50
|
+
for msg in mail:
|
|
51
|
+
if msg.name == "NAV_X":
|
|
52
|
+
self.nav_x = msg.double()
|
|
53
|
+
|
|
54
|
+
def iterate(self):
|
|
55
|
+
self.notify("MY_OUTPUT", self.nav_x * 2.0)
|
|
56
|
+
|
|
57
|
+
def build_report(self):
|
|
58
|
+
tab = ACTable(["Variable", "Value"])
|
|
59
|
+
tab.add("NAV_X", f"{self.nav_x:.2f}")
|
|
60
|
+
self.report(tab)
|
|
61
|
+
return True
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- Pure Python — no C++ compilation, no pybind11
|
|
67
|
+
- Full MOOS wire protocol (binary TCP, same as C++ clients)
|
|
68
|
+
- AppCasting and RealmCasting support
|
|
69
|
+
- Time warp support (`MOOSTimeWarp`)
|
|
70
|
+
- `ktm` (kill the moos) UDP multicast listener
|
|
71
|
+
- Mission file parsing (ProcessConfig blocks, global params)
|
|
72
|
+
- Geometry DTOs for all VIEW_* types (XYPoint, XYPolygon, XYCircle, etc.)
|
|
73
|
+
- NodeRecord parsing
|
|
74
|
+
- `py_compile` syntax checking at build time
|
|
75
|
+
- Works with pAntler — shebang'd scripts in `bin/`
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Python >= 3.8
|
|
80
|
+
- MOOSDB running (from moos-ivp)
|
|
81
|
+
- No other dependencies for core functionality
|
|
82
|
+
- `flask` for web-based viewer apps (optional)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pymoos2 — Pure-Python MOOS-IvP application framework.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pymoos2.core.AppCastingMOOSApp import AppCastingMOOSApp
|
|
6
|
+
from pymoos2.core.MOOSApp import MOOSApp
|
|
7
|
+
from pymoos2.core.MOOSMsg import MOOSMsg
|
|
8
|
+
from pymoos2.core.MOOSCommClient import MOOSCommClient
|
|
9
|
+
from pymoos2.core.MissionReader import MissionReader
|
|
10
|
+
from pymoos2.core.ACTable import ACTable
|
|
11
|
+
from pymoos2.core.time import moos_time, moos_local_time, moos_pause
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import signal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(app_class, app_name, mission_file):
|
|
18
|
+
"""
|
|
19
|
+
Launch a pymoos2 application.
|
|
20
|
+
"""
|
|
21
|
+
app = app_class()
|
|
22
|
+
app._initialize(app_name, mission_file)
|
|
23
|
+
|
|
24
|
+
def shutdown(sig, frame):
|
|
25
|
+
app._shutdown()
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
|
|
28
|
+
signal.signal(signal.SIGINT, shutdown)
|
|
29
|
+
signal.signal(signal.SIGTERM, shutdown)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
app._run()
|
|
33
|
+
except KeyboardInterrupt:
|
|
34
|
+
app._shutdown()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AppCastingMOOSApp",
|
|
39
|
+
"MOOSApp",
|
|
40
|
+
"MOOSMsg",
|
|
41
|
+
"MOOSCommClient",
|
|
42
|
+
"MissionReader",
|
|
43
|
+
"ACTable",
|
|
44
|
+
"moos_time",
|
|
45
|
+
"moos_local_time",
|
|
46
|
+
"moos_pause",
|
|
47
|
+
"run",
|
|
48
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pymoos2 CLI — application generator and example scaffolding.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 -m pymoos2 --GenAppCastingMOOSApp AppName [prefix] [author]
|
|
6
|
+
python3 -m pymoos2 --example
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import stat
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
args = sys.argv[1:]
|
|
17
|
+
|
|
18
|
+
if not args:
|
|
19
|
+
print_usage()
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
if args[0] == "--GenAppCastingMOOSApp":
|
|
23
|
+
gen_app(args[1:])
|
|
24
|
+
elif args[0] == "--example":
|
|
25
|
+
gen_example()
|
|
26
|
+
else:
|
|
27
|
+
print_usage()
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_usage():
|
|
32
|
+
print("pymoos2 — Pure-Python MOOS-IvP application framework")
|
|
33
|
+
print()
|
|
34
|
+
print("Usage:")
|
|
35
|
+
print(" python3 -m pymoos2 --GenAppCastingMOOSApp AppName [prefix] [author]")
|
|
36
|
+
print(" python3 -m pymoos2 --example")
|
|
37
|
+
print()
|
|
38
|
+
print("Options:")
|
|
39
|
+
print(" --GenAppCastingMOOSApp Generate a new MOOS app with AppCasting support")
|
|
40
|
+
print(" --example Generate a complete example mission with two pymoos2 apps")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def gen_app(remaining):
|
|
44
|
+
if not remaining:
|
|
45
|
+
print("Error: AppName is required.")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
app_name = remaining[0]
|
|
49
|
+
prefix = remaining[1] if len(remaining) > 1 else "p"
|
|
50
|
+
author = remaining[2] if len(remaining) > 2 else ""
|
|
51
|
+
|
|
52
|
+
full_name = f"{prefix}{app_name}"
|
|
53
|
+
out_dir = os.path.join(os.getcwd(), full_name)
|
|
54
|
+
|
|
55
|
+
if os.path.exists(out_dir):
|
|
56
|
+
print(f"Error: directory {full_name}/ already exists.")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
tmpl_dir = os.path.join(os.path.dirname(__file__), "templates")
|
|
60
|
+
replacements = {
|
|
61
|
+
"{{APP_NAME}}": app_name,
|
|
62
|
+
"{{PREFIX}}": prefix,
|
|
63
|
+
"{{AUTHOR}}": author,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def render(template_name):
|
|
67
|
+
with open(os.path.join(tmpl_dir, template_name), "r") as f:
|
|
68
|
+
content = f.read()
|
|
69
|
+
for k, v in replacements.items():
|
|
70
|
+
content = content.replace(k, v)
|
|
71
|
+
return content
|
|
72
|
+
|
|
73
|
+
os.makedirs(out_dir)
|
|
74
|
+
|
|
75
|
+
main_path = os.path.join(out_dir, f"{full_name}.py")
|
|
76
|
+
with open(main_path, "w") as f:
|
|
77
|
+
f.write(render("Main.py.tmpl"))
|
|
78
|
+
_make_executable(main_path)
|
|
79
|
+
|
|
80
|
+
with open(os.path.join(out_dir, f"{app_name}.py"), "w") as f:
|
|
81
|
+
f.write(render("App.py.tmpl"))
|
|
82
|
+
|
|
83
|
+
with open(os.path.join(out_dir, "CMakeLists.txt"), "w") as f:
|
|
84
|
+
f.write(render("CMakeLists.txt.tmpl"))
|
|
85
|
+
|
|
86
|
+
print(f"Generated {full_name}/")
|
|
87
|
+
print(f" {full_name}/{full_name}.py — entry point (executable)")
|
|
88
|
+
print(f" {full_name}/{app_name}.py — app class (edit this)")
|
|
89
|
+
print(f" {full_name}/CMakeLists.txt — CMake install rule")
|
|
90
|
+
print()
|
|
91
|
+
print(f"Next steps:")
|
|
92
|
+
print(f" 1. Edit {app_name}.py — implement on_startup, on_new_mail, iterate, build_report")
|
|
93
|
+
print(f" 2. Add to src/CMakeLists.txt: ADD_SUBDIRECTORY({full_name})")
|
|
94
|
+
print(f" 3. Build: cd build && cmake .. && make")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def gen_example():
|
|
98
|
+
cwd = os.getcwd()
|
|
99
|
+
src_dir = os.path.join(cwd, "src")
|
|
100
|
+
missions_dir = os.path.join(cwd, "missions")
|
|
101
|
+
|
|
102
|
+
# --- Pre-flight checks ---
|
|
103
|
+
print("pymoos2 --example")
|
|
104
|
+
print("=" * 50)
|
|
105
|
+
print()
|
|
106
|
+
print("You should be in moos-ivp-{your-extend-tree}/ with src/ and missions/")
|
|
107
|
+
print("below your current directory.")
|
|
108
|
+
print()
|
|
109
|
+
print(f" Current directory: {cwd}")
|
|
110
|
+
print()
|
|
111
|
+
print("This will generate:")
|
|
112
|
+
print()
|
|
113
|
+
print(" Apps (in src/):")
|
|
114
|
+
print(" pWptPulse/ — vehicle app: publishes VIEW_RANGE_PULSE on waypoint changes")
|
|
115
|
+
print(" pNodeWatch/ — shoreside app: web page showing scrolling NODE_REPORT log")
|
|
116
|
+
print()
|
|
117
|
+
print(" Mission (in missions/):")
|
|
118
|
+
print(" alpha_pymoos2/ — ufld_alpha-based mission with both pymoos2 apps wired in")
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
if not os.path.isdir(src_dir):
|
|
122
|
+
print(f"Error: no src/ directory found in {cwd}")
|
|
123
|
+
print(" Run this from the root of a moos-ivp-extend tree.")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
if not os.path.isdir(missions_dir):
|
|
126
|
+
print(f"Error: no missions/ directory found in {cwd}")
|
|
127
|
+
print(" Run this from the root of a moos-ivp-extend tree.")
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
# Check for conflicts
|
|
131
|
+
conflicts = []
|
|
132
|
+
for d in ["src/pWptPulse", "src/pNodeWatch", "missions/alpha_pymoos2"]:
|
|
133
|
+
if os.path.exists(os.path.join(cwd, d)):
|
|
134
|
+
conflicts.append(d)
|
|
135
|
+
if conflicts:
|
|
136
|
+
print("Error: the following already exist:")
|
|
137
|
+
for c in conflicts:
|
|
138
|
+
print(f" {c}/")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
# Confirm
|
|
142
|
+
resp = input("Proceed? [y/N] ").strip().lower()
|
|
143
|
+
if resp not in ("y", "yes"):
|
|
144
|
+
print("Aborted.")
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
|
|
147
|
+
# Check for flask dependency
|
|
148
|
+
try:
|
|
149
|
+
import flask
|
|
150
|
+
except ImportError:
|
|
151
|
+
print(" pNodeWatch requires flask. Installing...")
|
|
152
|
+
import subprocess
|
|
153
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "flask"],
|
|
154
|
+
stdout=subprocess.DEVNULL)
|
|
155
|
+
print(" flask installed.")
|
|
156
|
+
|
|
157
|
+
print()
|
|
158
|
+
example_dir = os.path.join(os.path.dirname(__file__), "templates", "example")
|
|
159
|
+
|
|
160
|
+
# --- Copy pWptPulse ---
|
|
161
|
+
_copytree(os.path.join(example_dir, "pWptPulse"), os.path.join(src_dir, "pWptPulse"))
|
|
162
|
+
_make_executable(os.path.join(src_dir, "pWptPulse", "pWptPulse.py"))
|
|
163
|
+
print(" Created src/pWptPulse/")
|
|
164
|
+
|
|
165
|
+
# --- Copy pNodeWatch ---
|
|
166
|
+
_copytree(os.path.join(example_dir, "pNodeWatch"), os.path.join(src_dir, "pNodeWatch"))
|
|
167
|
+
_make_executable(os.path.join(src_dir, "pNodeWatch", "pNodeWatch.py"))
|
|
168
|
+
print(" Created src/pNodeWatch/")
|
|
169
|
+
|
|
170
|
+
# --- Copy mission ---
|
|
171
|
+
_copytree(os.path.join(example_dir, "alpha_pymoos2"), os.path.join(missions_dir, "alpha_pymoos2"))
|
|
172
|
+
for sh in ["launch.sh", "launch_vehicle.sh", "launch_shoreside.sh", "clean.sh"]:
|
|
173
|
+
_make_executable(os.path.join(missions_dir, "alpha_pymoos2", sh))
|
|
174
|
+
print(" Created missions/alpha_pymoos2/")
|
|
175
|
+
|
|
176
|
+
print()
|
|
177
|
+
print("To complete setup, add the following to src/CMakeLists.txt:")
|
|
178
|
+
print()
|
|
179
|
+
print(" ADD_SUBDIRECTORY(pWptPulse)")
|
|
180
|
+
print(" ADD_SUBDIRECTORY(pNodeWatch)")
|
|
181
|
+
print()
|
|
182
|
+
print("Then:")
|
|
183
|
+
print(" 1. ./build.sh")
|
|
184
|
+
print(" 2. cd missions/alpha_pymoos2")
|
|
185
|
+
print(" 3. ./launch.sh 10 (launches at 10x time warp)")
|
|
186
|
+
print(" 4. Open http://localhost:8081 to see pNodeWatch")
|
|
187
|
+
print(" 5. Hit DEPLOY in pMarineViewer — watch for yellow range pulses at waypoints")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _copytree(src, dst):
|
|
191
|
+
"""Copy a directory tree."""
|
|
192
|
+
shutil.copytree(src, dst)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _make_executable(path):
|
|
196
|
+
st = os.stat(path)
|
|
197
|
+
os.chmod(path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Column-aligned ASCII tables for AppCast reports.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ACTable:
|
|
7
|
+
"""Build formatted column-aligned tables for buildReport()."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, columns, pad=2):
|
|
10
|
+
"""
|
|
11
|
+
columns: list of column header strings, e.g. ["Variable", "Value"]
|
|
12
|
+
pad: spaces between columns
|
|
13
|
+
"""
|
|
14
|
+
self.headers = list(columns)
|
|
15
|
+
self.ncols = len(columns)
|
|
16
|
+
self.pad = pad
|
|
17
|
+
self.rows = []
|
|
18
|
+
self.widths = [len(h) for h in self.headers]
|
|
19
|
+
|
|
20
|
+
def add(self, *values):
|
|
21
|
+
"""Add a row of values. Number of values should match number of columns."""
|
|
22
|
+
row = [str(v) for v in values]
|
|
23
|
+
# Pad or truncate to match column count
|
|
24
|
+
while len(row) < self.ncols:
|
|
25
|
+
row.append("")
|
|
26
|
+
row = row[:self.ncols]
|
|
27
|
+
self.rows.append(row)
|
|
28
|
+
for i, v in enumerate(row):
|
|
29
|
+
if len(v) > self.widths[i]:
|
|
30
|
+
self.widths[i] = len(v)
|
|
31
|
+
|
|
32
|
+
def _fmt_row(self, values):
|
|
33
|
+
parts = []
|
|
34
|
+
for i, v in enumerate(values):
|
|
35
|
+
parts.append(v.ljust(self.widths[i]))
|
|
36
|
+
sep = " " * self.pad
|
|
37
|
+
return sep.join(parts)
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
lines = []
|
|
41
|
+
lines.append(self._fmt_row(self.headers))
|
|
42
|
+
lines.append(self._fmt_row(["-" * self.widths[i] for i in range(self.ncols)]))
|
|
43
|
+
for row in self.rows:
|
|
44
|
+
lines.append(self._fmt_row(row))
|
|
45
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AppCastingMOOSApp — MOOSApp with AppCasting support.
|
|
3
|
+
|
|
4
|
+
Registers for APPCAST_REQ, posts APPCAST reports via build_report(),
|
|
5
|
+
and tracks events/warnings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pymoos2.core.time import moos_time
|
|
9
|
+
from pymoos2.core.MOOSApp import MOOSApp
|
|
10
|
+
|
|
11
|
+
# Separators used by the C++ AppCast serialization
|
|
12
|
+
OSEP = "!@#" # outer: between fields
|
|
13
|
+
ISEP = "!@" # inner: between items within a field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AppCastingMOOSApp(MOOSApp):
|
|
17
|
+
"""
|
|
18
|
+
MOOS application with AppCasting support.
|
|
19
|
+
Override: on_startup(), on_new_mail(), iterate(), build_report().
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self._report_lines = []
|
|
25
|
+
self._events = []
|
|
26
|
+
self._run_warnings = {} # warning_text -> count
|
|
27
|
+
self._config_warnings = []
|
|
28
|
+
self._max_events = 20
|
|
29
|
+
self._last_report_time = 0.0
|
|
30
|
+
|
|
31
|
+
# APPCAST_REQ tracking: key -> expiry_time
|
|
32
|
+
self._appcast_requests = {}
|
|
33
|
+
|
|
34
|
+
# --- Report building ---
|
|
35
|
+
|
|
36
|
+
def report(self, text):
|
|
37
|
+
"""Append a line (or multi-line string, or ACTable) to the current report."""
|
|
38
|
+
self._report_lines.append(str(text))
|
|
39
|
+
|
|
40
|
+
def report_event(self, event):
|
|
41
|
+
"""Add a timestamped event to the appcast."""
|
|
42
|
+
ts = moos_time() - self.m_start_time
|
|
43
|
+
self._events.append(f"[{ts:8.1f}] {event}")
|
|
44
|
+
if len(self._events) > self._max_events:
|
|
45
|
+
self._events = self._events[-self._max_events:]
|
|
46
|
+
|
|
47
|
+
def report_run_warning(self, warning):
|
|
48
|
+
"""Add a runtime warning (tracked with count)."""
|
|
49
|
+
self._run_warnings[warning] = self._run_warnings.get(warning, 0) + 1
|
|
50
|
+
|
|
51
|
+
def retract_run_warning(self, warning):
|
|
52
|
+
"""Remove a runtime warning."""
|
|
53
|
+
self._run_warnings.pop(warning, None)
|
|
54
|
+
|
|
55
|
+
def report_config_warning(self, warning):
|
|
56
|
+
"""Add a configuration warning."""
|
|
57
|
+
if warning not in self._config_warnings:
|
|
58
|
+
self._config_warnings.append(warning)
|
|
59
|
+
|
|
60
|
+
# --- Lifecycle callback (override this) ---
|
|
61
|
+
|
|
62
|
+
def build_report(self):
|
|
63
|
+
"""
|
|
64
|
+
Build the AppCast report. Override this.
|
|
65
|
+
Use self.report("text") and self.report(actable) to add content.
|
|
66
|
+
Return True to post the report.
|
|
67
|
+
"""
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
# --- Internal ---
|
|
71
|
+
|
|
72
|
+
def on_startup(self):
|
|
73
|
+
"""Override in subclass. Base registers for APPCAST_REQ."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _run(self):
|
|
77
|
+
"""Extend parent _run to register for APPCAST_REQ after startup."""
|
|
78
|
+
super_on_startup = self.on_startup
|
|
79
|
+
super_on_new_mail = self.on_new_mail
|
|
80
|
+
|
|
81
|
+
def wrapped_on_startup():
|
|
82
|
+
self.register("APPCAST_REQ", 0.0)
|
|
83
|
+
super_on_startup()
|
|
84
|
+
|
|
85
|
+
def wrapped_on_new_mail(mail):
|
|
86
|
+
user_mail = []
|
|
87
|
+
for msg in mail:
|
|
88
|
+
if msg.name == "APPCAST_REQ":
|
|
89
|
+
self._handle_appcast_req(msg.string())
|
|
90
|
+
else:
|
|
91
|
+
user_mail.append(msg)
|
|
92
|
+
if user_mail:
|
|
93
|
+
super_on_new_mail(user_mail)
|
|
94
|
+
|
|
95
|
+
self.on_startup = wrapped_on_startup
|
|
96
|
+
self.on_new_mail = wrapped_on_new_mail
|
|
97
|
+
super()._run()
|
|
98
|
+
|
|
99
|
+
def _handle_appcast_req(self, req_str):
|
|
100
|
+
"""Parse APPCAST_REQ and track if it targets this app."""
|
|
101
|
+
params = {}
|
|
102
|
+
for token in req_str.split(","):
|
|
103
|
+
if "=" in token:
|
|
104
|
+
k, v = token.split("=", 1)
|
|
105
|
+
params[k.strip().lower()] = v.strip()
|
|
106
|
+
|
|
107
|
+
req_node = params.get("node", "")
|
|
108
|
+
req_app = params.get("app", "")
|
|
109
|
+
duration = min(float(params.get("duration", "3")), 30.0)
|
|
110
|
+
key = params.get("key", "unknown")
|
|
111
|
+
|
|
112
|
+
if req_node and req_node != "all" and req_node != self.m_community:
|
|
113
|
+
return
|
|
114
|
+
if req_app and req_app != "all" and req_app != self.m_app_name:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
self._appcast_requests[key] = moos_time() + duration
|
|
118
|
+
|
|
119
|
+
def _appcast_requested(self):
|
|
120
|
+
"""Returns True if any non-expired appcast request exists."""
|
|
121
|
+
now = moos_time()
|
|
122
|
+
self._appcast_requests = {
|
|
123
|
+
k: v for k, v in self._appcast_requests.items() if v > now
|
|
124
|
+
}
|
|
125
|
+
return len(self._appcast_requests) > 0
|
|
126
|
+
|
|
127
|
+
def _post_iterate(self):
|
|
128
|
+
"""Build and post the AppCast report after each iteration."""
|
|
129
|
+
if not self._appcast_requested():
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
self._report_lines = []
|
|
133
|
+
should_post = self.build_report()
|
|
134
|
+
|
|
135
|
+
if should_post:
|
|
136
|
+
self._post_appcast()
|
|
137
|
+
self._last_report_time = moos_time()
|
|
138
|
+
|
|
139
|
+
def _post_appcast(self):
|
|
140
|
+
"""
|
|
141
|
+
Format and Notify the APPCAST string in the wire format
|
|
142
|
+
expected by uMAC / pMarineViewer.
|
|
143
|
+
"""
|
|
144
|
+
parts = []
|
|
145
|
+
|
|
146
|
+
# Required fields
|
|
147
|
+
parts.append(f"proc={self.m_app_name}")
|
|
148
|
+
parts.append(f"iter={self.m_iteration}")
|
|
149
|
+
parts.append(f"node={self.m_community}")
|
|
150
|
+
|
|
151
|
+
# Messages (the user's report content)
|
|
152
|
+
if self._report_lines:
|
|
153
|
+
msg_body = ISEP.join(self._report_lines)
|
|
154
|
+
parts.append(f"messages={msg_body}")
|
|
155
|
+
|
|
156
|
+
# Config warnings
|
|
157
|
+
if self._config_warnings:
|
|
158
|
+
parts.append("config_warnings=" + ISEP.join(self._config_warnings))
|
|
159
|
+
|
|
160
|
+
# Events
|
|
161
|
+
if self._events:
|
|
162
|
+
parts.append(f"events_total={len(self._events)}")
|
|
163
|
+
parts.append("events=" + ISEP.join(self._events))
|
|
164
|
+
|
|
165
|
+
# Run warnings (format: count:warning_text)
|
|
166
|
+
if self._run_warnings:
|
|
167
|
+
total = sum(self._run_warnings.values())
|
|
168
|
+
parts.append(f"run_warning_total={total}")
|
|
169
|
+
warnings_strs = [f"{count}:{text}" for text, count in self._run_warnings.items()]
|
|
170
|
+
parts.append("run_warnings=" + ISEP.join(warnings_strs))
|
|
171
|
+
|
|
172
|
+
appcast_str = OSEP.join(parts)
|
|
173
|
+
self.notify("APPCAST", appcast_str)
|
|
174
|
+
|
|
175
|
+
def _report_iterate_error(self, err_msg):
|
|
176
|
+
"""Surface iterate/on_new_mail exceptions as run warnings in AppCast."""
|
|
177
|
+
self.report_run_warning(err_msg)
|