Compare commits

..

No commits in common. "52d2aa52e20c1f26122766173c09a769015bc0a6" and "4bef10997a376522b4d5f614c88052a672301587" have entirely different histories.

7 changed files with 125 additions and 242 deletions

View File

@ -11,29 +11,25 @@
from aiohttp import web
import asyncio
from ament_index_python import get_package_share_directory
from am_i_up.Authenticator import Authenticator
import rclpy
class Api:
def __init__(self, facts):
self._facts = facts
self._authenticator = Authenticator(facts.get_password(), facts.get_open_endpoints())
async def run(self):
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
ui_static_directory = ui_share_directory + "/dist/assets"
app = web.Application(middlewares=[self._authenticator.build_middleware()])
app = web.Application()
app.add_routes([
web.get('/api/ping', self.ping),
web.get('/api/uptime', self.uptime),
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.post("/api/login", self.login),
web.static("/assets", ui_static_directory),
# we're not actually using key anywhere, but doing this allows react router
# to work correctly.
@ -53,23 +49,6 @@ class Api:
await asyncio.sleep(3600)
await runner.cleanup()
async def login(self, request):
request_dict = await request.json()
login_successful = self._authenticator.confirm_creds(request_dict["password"])
if not login_successful:
return web.Response(status=400)
redirect_location = f"{request.url.scheme}://{request.url.host_port_subcomponent}/"
redirect_header = {"Location": redirect_location}
response = web.Response(status=302, headers=redirect_header)
response.set_cookie(self._authenticator.cookie_name(),
self._authenticator.make_cookie_for_user(),
secure=False,
httponly=False,
samesite="Strict")
return response
async def index(self, request):
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
ui_index_path = ui_share_directory + "/dist/index.html"
@ -107,6 +86,13 @@ 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()
@ -118,13 +104,3 @@ 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)

View File

@ -1,47 +0,0 @@
from aiohttp import web
import secrets
import re
# TODO: Note here about how this is not supposed to be put on the
# internet and if you wanted to do that it needs to be done
# differently.
class Authenticator():
def __init__(self, password, open_endpoints):
self._password = password
self._open_endpoints = open_endpoints
self._valid_cookies = []
def confirm_creds(self, password):
return secrets.compare_digest(password, self._password)
def make_cookie_for_user(self):
cookie = secrets.token_urlsafe(32)
# TODO: Normally these would expire, but we're assuming
# this app gets closed like once a day so.
self._valid_cookies.append(cookie)
return cookie
def confirm_user_from_cookie(self, cookie):
return cookie in self._valid_cookies
def cookie_name(self):
return "auth_cookie"
def build_middleware(self):
@web.middleware
async def auth_middleware(request, handler):
for open_endpoint in self._open_endpoints:
if re.fullmatch(open_endpoint, request.path):
return await handler(request)
if self.cookie_name() in request.cookies:
cookie = request.cookies[self.cookie_name()]
if self.confirm_user_from_cookie(cookie):
return await handler(request)
redirect_location = f"{request.url.scheme}://{request.url.host_port_subcomponent}/login"
redirect_header = {"Location": redirect_location}
return web.Response(status=302, headers=redirect_header)
return auth_middleware

View File

@ -8,64 +8,39 @@
# 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
import j7s_diagnostics_py
import json
from ament_index_python import get_package_share_directory
from am_i_up.Ros import Ros
class Facts:
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._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._password = self._node.declare_parameter("password", "password").value
self._status_string = None
self._lat_long = None
self._diag_msg = None
self._ros = Ros()
self._start_time = time.monotonic()
self._status_string = None
async def run(self):
while rclpy.ok():
rclpy.spin_once(self._node, timeout_sec=0)
await asyncio.sleep(1e-4)
await self._ros.run()
def get_lat_long(self):
return self._lat_long
return self._ros.get_lat_long()
def get_costmap_image(self):
return self._ros.get_costmap_image()
def get_status(self):
return self._status_string
return self._ros.get_status_string()
def get_listen_port(self):
return self._listen_port
return self._ros.get_listen_port()
def get_ui_pkg(self):
return self._ui_pkg
def get_diagnostics_json(self):
if not self._diag_msg:
return None
return diag_msg_to_json(self._diag_msg)
return self._ros.get_ui_pkg()
def get_uptime(self):
return time.monotonic() - self._start_time
@ -116,63 +91,6 @@ class Facts:
print("Can't find build info.\n{}".format(e))
return project_state_content
def get_password(self):
return self._password
def get_open_endpoints(self):
# Hardcoding for now, can make a parameter later.
return ["/login", "/api/login", "/assets/(.*)"]
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:

64
am_i_up/Ros.py Normal file
View File

@ -0,0 +1,64 @@
#
# 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))

View File

@ -8,7 +8,6 @@
# 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
@ -16,77 +15,53 @@ import json
def main(args=None):
parser = argparse.ArgumentParser()
parser.add_argument("--action")
parser.add_argument("--options", nargs="*")
parser.add_argument("--host", default="http://localhost:8000")
args = parser.parse_args()
rclpy.init(args=args)
node = rclpy.create_node('am_i_up_client')
action = node.declare_parameter('action', value="").value
options = node.declare_parameter('options', value=[""]).value
client = Client(args.host)
if "" == action:
raise RunTimeError("Need to provide an action to take.")
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())
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())
class Client():
def __init__(self, host):
self._host = host
async def call_ping(options):
if len(options) != 1 or options[0] == "":
raise RunTimeError("Ping option is an address as a string.")
async def login(self, session):
request = { "password": "password" }
async with session.post(f'{self._host}/api/login', json=request) as resp:
if not resp.ok:
print("Login failed.")
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:
await self.login(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:
await self.login(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:
await self.login(session)
async with session.get(f'{self._host}/api/build_info') as resp:
print(await resp.text())
async def call_env(self):
print("Calling env.")
async with aiohttp.ClientSession() as session:
await self.login(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:
await self.login(session)
async with session.get(f'{self._host}/api/diagnostics') 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())

View File

@ -13,11 +13,9 @@ from am_i_up.Facts import Facts
import asyncio
async def main():
def main():
facts = Facts()
api = Api(facts)
future = asyncio.gather(api.run(), facts.run())
await future
asyncio.run(main())
asyncio.get_event_loop().run_until_complete(future)

View File

@ -12,7 +12,6 @@
<depend>python3-aiohttp</depend>
<!-- TODO: Run time dep on ping -->
<exec_depend>j7s_diagnostics_py</exec_depend>
<exec_depend>rclpy</exec_depend>
<export>