pymoos2 0.1.0__py3-none-any.whl

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