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.
- pymoos2/__init__.py +48 -0
- pymoos2/__main__.py +201 -0
- pymoos2/core/ACTable.py +45 -0
- pymoos2/core/AppCastingMOOSApp.py +177 -0
- pymoos2/core/MOOSApp.py +280 -0
- pymoos2/core/MOOSCommClient.py +236 -0
- pymoos2/core/MOOSCommPkt.py +36 -0
- pymoos2/core/MOOSMsg.py +144 -0
- pymoos2/core/MissionReader.py +107 -0
- pymoos2/core/__init__.py +0 -0
- pymoos2/core/constants.py +47 -0
- pymoos2/core/time.py +67 -0
- pymoos2/dtos/__init__.py +0 -0
- pymoos2/dtos/contacts/NodeRecord.py +150 -0
- pymoos2/dtos/contacts/__init__.py +0 -0
- pymoos2/dtos/geometry/XYArrow.py +38 -0
- pymoos2/dtos/geometry/XYCircle.py +38 -0
- pymoos2/dtos/geometry/XYCommsPulse.py +41 -0
- pymoos2/dtos/geometry/XYMarker.py +52 -0
- pymoos2/dtos/geometry/XYObject.py +99 -0
- pymoos2/dtos/geometry/XYOval.py +40 -0
- pymoos2/dtos/geometry/XYPoint.py +37 -0
- pymoos2/dtos/geometry/XYPolygon.py +48 -0
- pymoos2/dtos/geometry/XYRangePulse.py +44 -0
- pymoos2/dtos/geometry/XYSegList.py +60 -0
- pymoos2/dtos/geometry/XYTextBox.py +48 -0
- pymoos2/dtos/geometry/XYVector.py +41 -0
- pymoos2/dtos/geometry/XYWedge.py +37 -0
- pymoos2/dtos/geometry/__init__.py +13 -0
- pymoos2/templates/App.py.tmpl +115 -0
- pymoos2/templates/CMakeLists.txt.tmpl +21 -0
- pymoos2/templates/Main.py.tmpl +97 -0
- pymoos2/templates/example/alpha_pymoos2/clean.sh +36 -0
- pymoos2/templates/example/alpha_pymoos2/launch.sh +98 -0
- pymoos2/templates/example/alpha_pymoos2/launch_shoreside.sh +130 -0
- pymoos2/templates/example/alpha_pymoos2/launch_vehicle.sh +176 -0
- pymoos2/templates/example/alpha_pymoos2/meta_shoreside.moos +117 -0
- pymoos2/templates/example/alpha_pymoos2/meta_vehicle.bhv +56 -0
- pymoos2/templates/example/alpha_pymoos2/meta_vehicle.moos +175 -0
- pymoos2/templates/example/alpha_pymoos2/plugs.moos +59 -0
- pymoos2/templates/example/pNodeWatch/CMakeLists.txt +20 -0
- pymoos2/templates/example/pNodeWatch/NodeWatch.py +180 -0
- pymoos2/templates/example/pNodeWatch/__pycache__/NodeWatch.cpython-312.pyc +0 -0
- pymoos2/templates/example/pNodeWatch/__pycache__/pNodeWatch.cpython-312.pyc +0 -0
- pymoos2/templates/example/pNodeWatch/pNodeWatch.py +96 -0
- pymoos2/templates/example/pWptPulse/CMakeLists.txt +20 -0
- pymoos2/templates/example/pWptPulse/WptPulse.py +107 -0
- pymoos2/templates/example/pWptPulse/__pycache__/WptPulse.cpython-312.pyc +0 -0
- pymoos2/templates/example/pWptPulse/__pycache__/pWptPulse.cpython-312.pyc +0 -0
- pymoos2/templates/example/pWptPulse/pWptPulse.py +96 -0
- pymoos2-0.1.0.dist-info/LICENSE +21 -0
- pymoos2-0.1.0.dist-info/METADATA +102 -0
- pymoos2-0.1.0.dist-info/RECORD +56 -0
- pymoos2-0.1.0.dist-info/WHEEL +5 -0
- pymoos2-0.1.0.dist-info/entry_points.txt +2 -0
- 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()
|
pymoos2/core/ACTable.py
ADDED
|
@@ -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)
|