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.
Files changed (60) hide show
  1. pymoos2-0.1.0/LICENSE +21 -0
  2. pymoos2-0.1.0/PKG-INFO +102 -0
  3. pymoos2-0.1.0/README.md +82 -0
  4. pymoos2-0.1.0/pymoos2/__init__.py +48 -0
  5. pymoos2-0.1.0/pymoos2/__main__.py +201 -0
  6. pymoos2-0.1.0/pymoos2/core/ACTable.py +45 -0
  7. pymoos2-0.1.0/pymoos2/core/AppCastingMOOSApp.py +177 -0
  8. pymoos2-0.1.0/pymoos2/core/MOOSApp.py +280 -0
  9. pymoos2-0.1.0/pymoos2/core/MOOSCommClient.py +236 -0
  10. pymoos2-0.1.0/pymoos2/core/MOOSCommPkt.py +36 -0
  11. pymoos2-0.1.0/pymoos2/core/MOOSMsg.py +144 -0
  12. pymoos2-0.1.0/pymoos2/core/MissionReader.py +107 -0
  13. pymoos2-0.1.0/pymoos2/core/__init__.py +0 -0
  14. pymoos2-0.1.0/pymoos2/core/constants.py +47 -0
  15. pymoos2-0.1.0/pymoos2/core/time.py +67 -0
  16. pymoos2-0.1.0/pymoos2/dtos/__init__.py +0 -0
  17. pymoos2-0.1.0/pymoos2/dtos/contacts/NodeRecord.py +150 -0
  18. pymoos2-0.1.0/pymoos2/dtos/contacts/__init__.py +0 -0
  19. pymoos2-0.1.0/pymoos2/dtos/geometry/XYArrow.py +38 -0
  20. pymoos2-0.1.0/pymoos2/dtos/geometry/XYCircle.py +38 -0
  21. pymoos2-0.1.0/pymoos2/dtos/geometry/XYCommsPulse.py +41 -0
  22. pymoos2-0.1.0/pymoos2/dtos/geometry/XYMarker.py +52 -0
  23. pymoos2-0.1.0/pymoos2/dtos/geometry/XYObject.py +99 -0
  24. pymoos2-0.1.0/pymoos2/dtos/geometry/XYOval.py +40 -0
  25. pymoos2-0.1.0/pymoos2/dtos/geometry/XYPoint.py +37 -0
  26. pymoos2-0.1.0/pymoos2/dtos/geometry/XYPolygon.py +48 -0
  27. pymoos2-0.1.0/pymoos2/dtos/geometry/XYRangePulse.py +44 -0
  28. pymoos2-0.1.0/pymoos2/dtos/geometry/XYSegList.py +60 -0
  29. pymoos2-0.1.0/pymoos2/dtos/geometry/XYTextBox.py +48 -0
  30. pymoos2-0.1.0/pymoos2/dtos/geometry/XYVector.py +41 -0
  31. pymoos2-0.1.0/pymoos2/dtos/geometry/XYWedge.py +37 -0
  32. pymoos2-0.1.0/pymoos2/dtos/geometry/__init__.py +13 -0
  33. pymoos2-0.1.0/pymoos2/templates/App.py.tmpl +115 -0
  34. pymoos2-0.1.0/pymoos2/templates/CMakeLists.txt.tmpl +21 -0
  35. pymoos2-0.1.0/pymoos2/templates/Main.py.tmpl +97 -0
  36. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/clean.sh +36 -0
  37. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch.sh +98 -0
  38. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch_shoreside.sh +130 -0
  39. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/launch_vehicle.sh +176 -0
  40. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_shoreside.moos +117 -0
  41. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_vehicle.bhv +56 -0
  42. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/meta_vehicle.moos +175 -0
  43. pymoos2-0.1.0/pymoos2/templates/example/alpha_pymoos2/plugs.moos +59 -0
  44. pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/CMakeLists.txt +20 -0
  45. pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/NodeWatch.py +180 -0
  46. pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/__pycache__/NodeWatch.cpython-312.pyc +0 -0
  47. pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/__pycache__/pNodeWatch.cpython-312.pyc +0 -0
  48. pymoos2-0.1.0/pymoos2/templates/example/pNodeWatch/pNodeWatch.py +96 -0
  49. pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/CMakeLists.txt +20 -0
  50. pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/WptPulse.py +107 -0
  51. pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/__pycache__/WptPulse.cpython-312.pyc +0 -0
  52. pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/__pycache__/pWptPulse.cpython-312.pyc +0 -0
  53. pymoos2-0.1.0/pymoos2/templates/example/pWptPulse/pWptPulse.py +96 -0
  54. pymoos2-0.1.0/pymoos2.egg-info/PKG-INFO +102 -0
  55. pymoos2-0.1.0/pymoos2.egg-info/SOURCES.txt +58 -0
  56. pymoos2-0.1.0/pymoos2.egg-info/dependency_links.txt +1 -0
  57. pymoos2-0.1.0/pymoos2.egg-info/entry_points.txt +2 -0
  58. pymoos2-0.1.0/pymoos2.egg-info/top_level.txt +1 -0
  59. pymoos2-0.1.0/pyproject.toml +37 -0
  60. 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)
@@ -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)