diff --git a/am_i_up/Api.py b/am_i_up/Api.py index 7550567..3eab44a 100644 --- a/am_i_up/Api.py +++ b/am_i_up/Api.py @@ -28,8 +28,8 @@ class Api: web.get('/api/build_info', self.build_info), web.get('/api/env', self.env), web.get('/api/status', self.status), - web.get('/api/costmap_image', self.costmap_image), web.get("/api/position", self.position), + web.get("/api/diagnostics", self.diagnostics), web.static("/assets", ui_static_directory), # we're not actually using key anywhere, but doing this allows react router # to work correctly. @@ -86,13 +86,6 @@ class Api: resp = {"message": status} return web.json_response(resp) - async def costmap_image(self, request): - (image_format, image_data) = self._facts.get_costmap_image() - if (not image_format) or (not image_data): - raise web.HTTPUnsupportedMediaType() - resp = web.Response(body=image_data, content_type='image/{}'.format(image_format)) - return resp - async def position(self, request): resp = {} (lat, lon) = self._facts.get_lat_long() @@ -104,3 +97,13 @@ class Api: resp["longitude"] = lon return web.json_response(resp) + async def diagnostics(self, request): + resp = {} + diagnostics = self._facts.get_diagnostics_json() + if not diagnostics: + resp["status"] = False + return web.json_response(resp) + resp["status"] = True + resp["message"] = diagnostics + return web.json_response(resp) + diff --git a/am_i_up/Facts.py b/am_i_up/Facts.py index c089143..afa51f1 100644 --- a/am_i_up/Facts.py +++ b/am_i_up/Facts.py @@ -8,39 +8,62 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. # +import rclpy +from ament_index_python import get_package_share_directory +from std_msgs.msg import String +from sensor_msgs.msg import CompressedImage +from sensor_msgs.msg import NavSatFix +from diagnostic_msgs.msg import DiagnosticArray +from diagnostic_msgs.msg import DiagnosticStatus +import asyncio import time import ipaddress import subprocess import os import yaml -from ament_index_python import get_package_share_directory - -from am_i_up.Ros import Ros +import j7s_diagnostics_py +import json class Facts: def __init__(self): - self._ros = Ros() + + rclpy.init() + self._node = rclpy.create_node('am_i_up') + + self._status_sub = self._node.create_subscription(String, "status", self._status_callback, 1) + self._navsat_sub = self._node.create_subscription(NavSatFix, "navsatfix", self._navsat_callback, 1) + self._diagnostics_sub = self._node.create_subscription(DiagnosticArray, "/diagnostics_agg", self._diag_callback, 1) + + self._listen_port = self._node.declare_parameter("listen_port", "8888").value + self._ui_pkg = self._node.declare_parameter("ui_pkg", "am_i_up_ui").value + + self._status_string = None + self._lat_long = None + self._diag_msg = None self._start_time = time.monotonic() - self._status_string = None async def run(self): - await self._ros.run() + while rclpy.ok(): + rclpy.spin_once(self._node, timeout_sec=0) + await asyncio.sleep(1e-4) def get_lat_long(self): - return self._ros.get_lat_long() - - def get_costmap_image(self): - return self._ros.get_costmap_image() + return self._lat_long def get_status(self): - return self._ros.get_status_string() + return self._status_string def get_listen_port(self): - return self._ros.get_listen_port() + return self._listen_port def get_ui_pkg(self): - return self._ros.get_ui_pkg() + return self._ui_pkg + + def get_diagnostics_json(self): + if not self._diag_msg: + return None + return diag_msg_to_json(self._diag_msg) def get_uptime(self): return time.monotonic() - self._start_time @@ -91,6 +114,56 @@ class Facts: print("Can't find build info.\n{}".format(e)) return project_state_content + def _status_callback(self, msg): + self._status_string = msg.data + + def _diag_callback(self, msg): + self._diag_msg = msg + + def _navsat_callback(self, msg): + self._lat_long = (msg.latitude, msg.longitude) + +def diag_level_to_rust(level): + if level == DiagnosticStatus.OK: + return j7s_diagnostics_py.DiagnosticLevel.OK + if level == DiagnosticStatus.WARN: + return j7s_diagnostics_py.DiagnosticLevel.WARN + if level == DiagnosticStatus.ERROR: + return j7s_diagnostics_py.DiagnosticLevel.ERROR + if level == DiagnosticStatus.STALE: + return j7s_diagnostics_py.DiagnosticLevel.STALE + return j7s_diagnostics_py.DiagnosticLevel.UNSET + +def key_values_to_dict(values): + to_return = {} + for value in values: + to_return[value.key] = value.value + return to_return + +def diag_status_to_rust(status): + rust = j7s_diagnostics_py.DiagnosticStatus() + rust.level = diag_level_to_rust(status.level) + rust.name = status.name + rust.message = status.message + rust.hardware_id = status.hardware_id + rust.values = key_values_to_dict(status.values) + + return rust + +def diag_msg_to_json(diagnostic_array): + # Make a DiagnosticTree. + tree = j7s_diagnostics_py.DiagnosticsTree() + # Iterate through the statuses in message. + for status in diagnostic_array.status: + # Convert the statues to the type used by the tree. + rust_status = diag_status_to_rust(status) + # Insert them into the tree. + tree.insert(rust_status) + # Call the to json function. + tree.reconcile_levels() + json_string = tree.to_json() + return json.loads(json_string) + def is_valid_ip(address): try: diff --git a/am_i_up/Ros.py b/am_i_up/Ros.py deleted file mode 100644 index 5546565..0000000 --- a/am_i_up/Ros.py +++ /dev/null @@ -1,64 +0,0 @@ -# -# Copyright 2025 James Pace -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. -# -import rclpy -from std_msgs.msg import String -from sensor_msgs.msg import CompressedImage -from sensor_msgs.msg import NavSatFix -import asyncio - -class Ros: - def __init__(self): - rclpy.init() - self._node = rclpy.create_node('am_i_up') - - self._status_sub = self._node.create_subscription(String, "status", self._status_callback, 1) - self._image_sub = self._node.create_subscription(CompressedImage, "image/compressed", self._image_callback, 1) - self._navsat_sub = self._node.create_subscription(NavSatFix, "navsatfix", self._navsat_callback, 1) - - self._listen_port = self._node.declare_parameter("listen_port", "8888").value - self._ui_pkg = self._node.declare_parameter("ui_pkg", "am_i_up_ui").value - - self._status_string = None - self._lat_long = None - self._costmap_image = None - - def get_status_string(self): - return self._status_string - - def get_lat_long(self): - return self._lat_long - - def get_costmap_image(self): - return self._costmap_image - - def get_listen_port(self): - return self._listen_port - - def get_ui_pkg(self): - return self._ui_pkg - - async def run(self): - while rclpy.ok(): - rclpy.spin_once(self._node, timeout_sec=0) - await asyncio.sleep(1e-4) - - def _status_callback(self, msg): - self._status_string = msg.data - - def _navsat_callback(self, msg): - self._lat_long = (msg.latitude, msg.longitude) - - def _image_callback(self, msg): - if msg.format != "jpg" and msg.format != "png": - # I don't know what to do with this image.... - return - self._costmap_image = (msg.format, bytes(msg.data)) - diff --git a/am_i_up/client.py b/am_i_up/client.py index ff34f01..dad55da 100644 --- a/am_i_up/client.py +++ b/am_i_up/client.py @@ -8,6 +8,7 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. # +import argparse import aiohttp import asyncio import rclpy @@ -15,53 +16,66 @@ import json def main(args=None): - rclpy.init(args=args) - node = rclpy.create_node('am_i_up_client') + parser = argparse.ArgumentParser() + parser.add_argument("--action") + parser.add_argument("--options", nargs="*") + parser.add_argument("--host", default="http://localhost:8888") + args = parser.parse_args() - action = node.declare_parameter('action', value="").value - options = node.declare_parameter('options', value=[""]).value - if "" == action: - raise RunTimeError("Need to provide an action to take.") + client = Client(args.host) - if action == 'ping': - asyncio.run(call_ping(options)) - if action == 'uptime': - asyncio.run(call_uptime()) - if action == 'build_info': - asyncio.run(call_build_info()) - if action == 'env': - asyncio.run(call_env()) + if args.action == 'ping': + asyncio.run(client.call_ping(args.options)) + if args.action == 'uptime': + asyncio.run(client.call_uptime()) + if args.action == 'build_info': + asyncio.run(client.call_build_info()) + if args.action == 'env': + asyncio.run(client.call_env()) + if args.action == 'diagnostics': + asyncio.run(client.call_diagnostics()) -async def call_ping(options): - if len(options) != 1 or options[0] == "": - raise RunTimeError("Ping option is an address as a string.") +class Client(): + def __init__(self, host): + self._host = host - request = {"address": options[0]} + async def call_ping(self, options): + if len(options) != 1 or options[0] == "": + raise RunTimeError("Ping option is an address as a string.") - print("Calling ping with request: {}", json.dumps(request)) + request = {"address": options[0]} - async with aiohttp.ClientSession() as session: - async with session.get('http://localhost:8888/api/ping', json=request) as resp: - print(await resp.text()) + print("Calling ping with request: {}", json.dumps(request)) -async def call_uptime(): - print("Calling uptime.") + async with aiohttp.ClientSession() as session: + async with session.get(f'{self._host}/api/ping', json=request) as resp: + print(await resp.text()) - async with aiohttp.ClientSession() as session: - async with session.get('http://localhost:8888/api/uptime') as resp: - print(await resp.text()) + async def call_uptime(self): + print("Calling uptime.") -async def call_build_info(): - print("Calling build_info.") + async with aiohttp.ClientSession() as session: + async with session.get(f'{self._host}/api/uptime') as resp: + print(await resp.text()) - async with aiohttp.ClientSession() as session: - async with session.get('http://localhost:8888/api/build_info') as resp: - print(await resp.text()) + async def call_build_info(self): + print("Calling build_info.") -async def call_env(): - print("Calling env.") + async with aiohttp.ClientSession() as session: + async with session.get(f'{self._host}/api/build_info') as resp: + print(await resp.text()) - async with aiohttp.ClientSession() as session: - async with session.get('http://localhost:8888/api/env') as resp: - print(await resp.text()) + async def call_env(self): + print("Calling env.") + + async with aiohttp.ClientSession() as session: + async with session.get(f'{self._host}/api/env') as resp: + print(await resp.text()) + + async def call_diagnostics(self): + print("Calling diagnostics.") + + async with aiohttp.ClientSession() as session: + async with session.get(f'{self._host}/api/diagnostics') as resp: + print(await resp.text()) diff --git a/am_i_up/server.py b/am_i_up/server.py index ac60ca2..eba883c 100644 --- a/am_i_up/server.py +++ b/am_i_up/server.py @@ -13,9 +13,11 @@ from am_i_up.Facts import Facts import asyncio -def main(): +async def main(): facts = Facts() api = Api(facts) future = asyncio.gather(api.run(), facts.run()) - asyncio.get_event_loop().run_until_complete(future) + await future + +asyncio.run(main()) diff --git a/package.xml b/package.xml index 6d1ab6c..f910ebf 100644 --- a/package.xml +++ b/package.xml @@ -12,6 +12,7 @@ python3-aiohttp + j7s_diagnostics_py rclpy