Compare commits
7
Commits
4bef10997a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f9ca762be | ||
|
|
991a44b332 | ||
|
|
2d5e570625 | ||
|
|
28fc4e0f68 | ||
|
|
01523517d2 | ||
|
|
52d2aa52e2 | ||
|
|
f51b1cf4c0 |
No files matched your search
+1
-1
@@ -14,7 +14,7 @@ install(DIRECTORY
|
|||||||
|
|
||||||
# Install launch and param files.
|
# Install launch and param files.
|
||||||
install(DIRECTORY
|
install(DIRECTORY
|
||||||
launch params
|
launch params assets
|
||||||
DESTINATION share/${PROJECT_NAME}/
|
DESTINATION share/${PROJECT_NAME}/
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+107
-16
@@ -11,29 +11,44 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import asyncio
|
import asyncio
|
||||||
from ament_index_python import get_package_share_directory
|
from ament_index_python import get_package_share_directory
|
||||||
|
from am_i_up.Authenticator import Authenticator
|
||||||
|
from am_i_up.MediaMTXAuthenticator import MediaMTXAuthenticator
|
||||||
import rclpy
|
import rclpy
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
def __init__(self, facts):
|
def __init__(self, facts):
|
||||||
self._facts = facts
|
self._facts = facts
|
||||||
|
|
||||||
|
self._authenticator = Authenticator(facts.get_password(), self.get_open_endpoints())
|
||||||
|
self._mediamtx_authenticator = MediaMTXAuthenticator()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
|
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
|
||||||
ui_static_directory = ui_share_directory + "/dist/assets"
|
ui_static_directory = ui_share_directory + "/dist/assets"
|
||||||
|
|
||||||
app = web.Application()
|
server_static_directory = get_package_share_directory("am_i_up") + "/assets"
|
||||||
|
|
||||||
|
app = web.Application(middlewares=[self._authenticator.build_middleware()])
|
||||||
app.add_routes([
|
app.add_routes([
|
||||||
web.get('/api/ping', self.ping),
|
web.get('/api/ping', self.ping),
|
||||||
web.get('/api/uptime', self.uptime),
|
web.get('/api/uptime', self.uptime),
|
||||||
web.get('/api/build_info', self.build_info),
|
web.get('/api/build_info', self.build_info),
|
||||||
|
web.get('/api/version_number', self.version_number),
|
||||||
web.get('/api/env', self.env),
|
web.get('/api/env', self.env),
|
||||||
web.get('/api/status', self.status),
|
web.get('/api/identity', self.identity),
|
||||||
web.get('/api/costmap_image', self.costmap_image),
|
|
||||||
web.get("/api/position", self.position),
|
web.get("/api/position", self.position),
|
||||||
|
web.get("/api/diagnostics", self.diagnostics),
|
||||||
|
web.post("/api/login", self.login),
|
||||||
|
web.get("/api/available_streams", self.available_streams),
|
||||||
|
web.post("/api/mediamtx/auth", self.mediamtx_auth),
|
||||||
|
web.get("/api/mediamtx/login", self.mediamtx_login),
|
||||||
web.static("/assets", ui_static_directory),
|
web.static("/assets", ui_static_directory),
|
||||||
|
web.static("/server_assets", server_static_directory),
|
||||||
|
web.get('/login', self.login_page),
|
||||||
# we're not actually using key anywhere, but doing this allows react router
|
# we're not actually using key anywhere, but doing this allows react router
|
||||||
# to work correctly.
|
# to work correctly.
|
||||||
web.get("/{key:.*}", self.index)
|
web.get("/{key}", self.index),
|
||||||
|
web.get("/", self.index)
|
||||||
])
|
])
|
||||||
|
|
||||||
url = "0.0.0.0"
|
url = "0.0.0.0"
|
||||||
@@ -49,6 +64,62 @@ class Api:
|
|||||||
await asyncio.sleep(3600)
|
await asyncio.sleep(3600)
|
||||||
await runner.cleanup()
|
await runner.cleanup()
|
||||||
|
|
||||||
|
def get_open_endpoints(self):
|
||||||
|
return ["/login", "/api/login", "/server_assets/(.*)", "/api/mediamtx/auth"]
|
||||||
|
|
||||||
|
async def login_page(self, request):
|
||||||
|
login_page = get_package_share_directory("am_i_up") + "/assets/login.html"
|
||||||
|
return web.FileResponse(login_page)
|
||||||
|
|
||||||
|
async def mediamtx_auth(self, request):
|
||||||
|
# Take the mediamtx json and figure out if I'm a valid user.
|
||||||
|
# MediaMTX json looks like:
|
||||||
|
# {
|
||||||
|
# "user": "user",
|
||||||
|
# "password": "password",
|
||||||
|
# "token": "token",
|
||||||
|
# "ip": "ip",
|
||||||
|
# "action": "publish|read|playback|api|metrics|pprof",
|
||||||
|
# "path": "path",
|
||||||
|
# "protocol": "rtsp|rtmp|hls|webrtc|srt",
|
||||||
|
# "id": "id",
|
||||||
|
# "query": "query",
|
||||||
|
# "userAgent": "userAgent"
|
||||||
|
# }
|
||||||
|
# This route will be unprotected.
|
||||||
|
request_dict = await request.json()
|
||||||
|
print("Got request: {}", request_dict)
|
||||||
|
# Only allowing reads for now.
|
||||||
|
if request_dict["action"] != "read":
|
||||||
|
return web.Response(status=401)
|
||||||
|
valid_token = self._mediamtx_authenticator.confirm_token(request_dict["token"])
|
||||||
|
if valid_token:
|
||||||
|
return web.Response(status=200)
|
||||||
|
return web.Response(status=401)
|
||||||
|
|
||||||
|
async def mediamtx_login(self, request):
|
||||||
|
# Generate a valid token for the mediamtx stuff to use to login
|
||||||
|
# with.
|
||||||
|
token = self._mediamtx_authenticator.make_token()
|
||||||
|
resp = {"token": token}
|
||||||
|
return web.json_response(resp)
|
||||||
|
|
||||||
|
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):
|
async def index(self, request):
|
||||||
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
|
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
|
||||||
ui_index_path = ui_share_directory + "/dist/index.html"
|
ui_index_path = ui_share_directory + "/dist/index.html"
|
||||||
@@ -64,6 +135,17 @@ class Api:
|
|||||||
resp = {"uptime": self._facts.get_uptime()}
|
resp = {"uptime": self._facts.get_uptime()}
|
||||||
return web.json_response(resp)
|
return web.json_response(resp)
|
||||||
|
|
||||||
|
async def version_number(self, request):
|
||||||
|
version = self._facts.get_version()
|
||||||
|
if not version:
|
||||||
|
resp = {"status": False, "message": "version not found."}
|
||||||
|
return web.json_response(resp)
|
||||||
|
resp = {
|
||||||
|
"status": True,
|
||||||
|
"version": version
|
||||||
|
}
|
||||||
|
return web.json_response(resp)
|
||||||
|
|
||||||
async def build_info(self, request):
|
async def build_info(self, request):
|
||||||
project_state = self._facts.get_buildinfo()
|
project_state = self._facts.get_buildinfo()
|
||||||
if not project_state:
|
if not project_state:
|
||||||
@@ -79,20 +161,13 @@ class Api:
|
|||||||
env = self._facts.get_env()
|
env = self._facts.get_env()
|
||||||
return web.json_response(env)
|
return web.json_response(env)
|
||||||
|
|
||||||
async def status(self, request):
|
async def identity(self, request):
|
||||||
status = self._facts.get_status()
|
identity = self._facts.get_identity()
|
||||||
if not status:
|
if not identity:
|
||||||
status = "Nothing received!"
|
identity = "Nothing received!"
|
||||||
resp = {"message": status}
|
resp = {"message": identity}
|
||||||
return web.json_response(resp)
|
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):
|
async def position(self, request):
|
||||||
resp = {}
|
resp = {}
|
||||||
(lat, lon) = self._facts.get_lat_long()
|
(lat, lon) = self._facts.get_lat_long()
|
||||||
@@ -104,3 +179,19 @@ class Api:
|
|||||||
resp["longitude"] = lon
|
resp["longitude"] = lon
|
||||||
return web.json_response(resp)
|
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)
|
||||||
|
|
||||||
|
async def available_streams(self, request):
|
||||||
|
resp = {}
|
||||||
|
resp["streams"] = self._facts.get_video_streams()
|
||||||
|
resp["port"] = self._facts.get_video_display_port()
|
||||||
|
return web.json_response(resp)
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
+121
-14
@@ -8,39 +8,67 @@
|
|||||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
# defined by the Mozilla Public License, v. 2.0.
|
# 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 time
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
from ament_index_python import get_package_share_directory
|
import j7s_diagnostics_py
|
||||||
|
import json
|
||||||
from am_i_up.Ros import Ros
|
|
||||||
|
|
||||||
class Facts:
|
class Facts:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ros = Ros()
|
|
||||||
|
rclpy.init()
|
||||||
|
self._node = rclpy.create_node('am_i_up')
|
||||||
|
|
||||||
|
self._ident_sub = self._node.create_subscription(String, "identity", self._identity_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._video_streams = self._node.declare_parameter("video_streams", [""]).value
|
||||||
|
self._video_display_port = self._node.declare_parameter("video_display_port", "8889").value
|
||||||
|
|
||||||
|
self._identity_string = None
|
||||||
|
self._lat_long = None
|
||||||
|
self._diag_msg = None
|
||||||
|
|
||||||
self._start_time = time.monotonic()
|
self._start_time = time.monotonic()
|
||||||
self._status_string = None
|
|
||||||
|
|
||||||
async def run(self):
|
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):
|
def get_lat_long(self):
|
||||||
return self._ros.get_lat_long()
|
return self._lat_long
|
||||||
|
|
||||||
def get_costmap_image(self):
|
def get_identity(self):
|
||||||
return self._ros.get_costmap_image()
|
return self._identity_string
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
return self._ros.get_status_string()
|
|
||||||
|
|
||||||
def get_listen_port(self):
|
def get_listen_port(self):
|
||||||
return self._ros.get_listen_port()
|
return self._listen_port
|
||||||
|
|
||||||
def get_ui_pkg(self):
|
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):
|
def get_uptime(self):
|
||||||
return time.monotonic() - self._start_time
|
return time.monotonic() - self._start_time
|
||||||
@@ -91,6 +119,85 @@ class Facts:
|
|||||||
print("Can't find build info.\n{}".format(e))
|
print("Can't find build info.\n{}".format(e))
|
||||||
return project_state_content
|
return project_state_content
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
# Find the share directory for 'build_info_getter'.
|
||||||
|
build_info_getter_directory = None
|
||||||
|
try:
|
||||||
|
build_info_getter_directory = get_package_share_directory('build_info_getter')
|
||||||
|
except Exception as e:
|
||||||
|
print("Can't find build info.\n{}".format(e))
|
||||||
|
return None
|
||||||
|
# Find and read the version file in int.
|
||||||
|
version_number_file = build_info_getter_directory + "/version_number"
|
||||||
|
version_number = None
|
||||||
|
try:
|
||||||
|
with open(version_number_file, 'r') as file_obj:
|
||||||
|
version_number = file_obj.readline().rstrip()
|
||||||
|
except Exception as e:
|
||||||
|
# We either didn't load the file or couldn't read it.
|
||||||
|
print("Can't find build info.\n{}".format(e))
|
||||||
|
|
||||||
|
return version_number
|
||||||
|
|
||||||
|
def get_password(self):
|
||||||
|
return self._password
|
||||||
|
|
||||||
|
def get_video_streams(self):
|
||||||
|
return self._video_streams
|
||||||
|
|
||||||
|
def get_video_display_port(self):
|
||||||
|
return self._video_display_port
|
||||||
|
|
||||||
|
def _identity_callback(self, msg):
|
||||||
|
self._identity_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):
|
def is_valid_ip(address):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
class MediaMTXAuthenticator():
|
||||||
|
def __init__(self):
|
||||||
|
# Type: Dict["token"] -> expiration time
|
||||||
|
self._valid_tokens = {}
|
||||||
|
|
||||||
|
def confirm_token(self, token):
|
||||||
|
if not token in self._valid_tokens.keys():
|
||||||
|
return False
|
||||||
|
token_expiration_time = self._valid_tokens[token]
|
||||||
|
now = datetime.now()
|
||||||
|
if now < token_expiration_time:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def make_token(self):
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
expiration_time = datetime.now() + timedelta(minutes=1)
|
||||||
|
self._valid_tokens[token] = expiration_time
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
@@ -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))
|
|
||||||
|
|
||||||
+81
-37
@@ -8,6 +8,7 @@
|
|||||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
# defined by the Mozilla Public License, v. 2.0.
|
# defined by the Mozilla Public License, v. 2.0.
|
||||||
#
|
#
|
||||||
|
import argparse
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
import rclpy
|
import rclpy
|
||||||
@@ -15,53 +16,96 @@ import json
|
|||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
|
|
||||||
rclpy.init(args=args)
|
parser = argparse.ArgumentParser()
|
||||||
node = rclpy.create_node('am_i_up_client')
|
parser.add_argument("--action")
|
||||||
|
parser.add_argument("--options", nargs="*")
|
||||||
|
parser.add_argument("--host", default="http://localhost:8000")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
action = node.declare_parameter('action', value="").value
|
client = Client(args.host)
|
||||||
options = node.declare_parameter('options', value=[""]).value
|
|
||||||
|
|
||||||
if "" == action:
|
if args.action == 'ping':
|
||||||
raise RunTimeError("Need to provide an action to take.")
|
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 args.action == 'identity':
|
||||||
|
asyncio.run(client.call_identity())
|
||||||
|
if args.action == 'version':
|
||||||
|
asyncio.run(client.call_version())
|
||||||
|
|
||||||
if action == 'ping':
|
class Client():
|
||||||
asyncio.run(call_ping(options))
|
def __init__(self, host):
|
||||||
if action == 'uptime':
|
self._host = host
|
||||||
asyncio.run(call_uptime())
|
|
||||||
if action == 'build_info':
|
|
||||||
asyncio.run(call_build_info())
|
|
||||||
if action == 'env':
|
|
||||||
asyncio.run(call_env())
|
|
||||||
|
|
||||||
async def call_ping(options):
|
async def login(self, session):
|
||||||
if len(options) != 1 or options[0] == "":
|
request = { "password": "password" }
|
||||||
raise RunTimeError("Ping option is an address as a string.")
|
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:
|
print("Calling ping with request: {}", json.dumps(request))
|
||||||
async with session.get('http://localhost:8888/api/ping', json=request) as resp:
|
|
||||||
print(await resp.text())
|
|
||||||
|
|
||||||
async def call_uptime():
|
async with aiohttp.ClientSession() as session:
|
||||||
print("Calling uptime.")
|
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 def call_uptime(self):
|
||||||
async with session.get('http://localhost:8888/api/uptime') as resp:
|
print("Calling uptime.")
|
||||||
print(await resp.text())
|
|
||||||
|
|
||||||
async def call_build_info():
|
async with aiohttp.ClientSession() as session:
|
||||||
print("Calling build_info.")
|
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 def call_build_info(self):
|
||||||
async with session.get('http://localhost:8888/api/build_info') as resp:
|
print("Calling build_info.")
|
||||||
print(await resp.text())
|
|
||||||
|
|
||||||
async def call_env():
|
async with aiohttp.ClientSession() as session:
|
||||||
print("Calling env.")
|
await self.login(session)
|
||||||
|
async with session.get(f'{self._host}/api/build_info') as resp:
|
||||||
|
print(await resp.text())
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async def call_env(self):
|
||||||
async with session.get('http://localhost:8888/api/env') as resp:
|
print("Calling env.")
|
||||||
print(await resp.text())
|
|
||||||
|
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_identity(self):
|
||||||
|
print("Calling identity.")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
await self.login(session)
|
||||||
|
async with session.get(f'{self._host}/api/identity') as resp:
|
||||||
|
print(await resp.text())
|
||||||
|
|
||||||
|
async def call_version(self):
|
||||||
|
print("Calling version.")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
await self.login(session)
|
||||||
|
async with session.get(f'{self._host}/api/version_number') 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())
|
||||||
+4
-2
@@ -13,9 +13,11 @@ from am_i_up.Facts import Facts
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
def main():
|
async def main():
|
||||||
facts = Facts()
|
facts = Facts()
|
||||||
api = Api(facts)
|
api = Api(facts)
|
||||||
|
|
||||||
future = asyncio.gather(api.run(), facts.run())
|
future = asyncio.gather(api.run(), facts.run())
|
||||||
asyncio.get_event_loop().run_until_complete(future)
|
await future
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
Vendored
+6312
File diff suppressed because it is too large.
Load diff
Vendored
+12048
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Login</title>
|
||||||
|
<link href="server_assets/bootstrap.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.vert-padded {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="container vert-padded">
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="passwordForm" class="form-label">Password</label>
|
||||||
|
<input name="password" type="password" class="form-control" id="passwordForm" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
console.log("Hi Developer! This page lives with the server package NOT the ui package.");
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', async (event) => {
|
||||||
|
console.log("submit");
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
console.log(event.target);
|
||||||
|
const formEntries = new FormData(event.target);
|
||||||
|
const formData = Object.fromEntries(formEntries);
|
||||||
|
console.log(formData);
|
||||||
|
|
||||||
|
const resp = await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
if (resp.redirected) {
|
||||||
|
window.location.assign("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script src="server_assets/bootstrap.bundle.js"</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,9 +6,23 @@
|
|||||||
<param from="$(find-pkg-share am_i_up)/params/image2rtsp.yaml"/>
|
<param from="$(find-pkg-share am_i_up)/params/image2rtsp.yaml"/>
|
||||||
</node>
|
</node>
|
||||||
|
|
||||||
|
<!-- j7s threed -->
|
||||||
|
<node pkg="data_simulator" exec="frame_publisher" name="frame_publisher" />
|
||||||
|
<node pkg="j7s_threed" exec="j7s_threed_node" name="threed_node">
|
||||||
|
<param from="$(find-pkg-share j7s_threed)/params/threed_world.yaml" allow_substs="true"/>
|
||||||
|
<remap from="image" to="threed_image"/>
|
||||||
|
</node>
|
||||||
|
<node pkg="image2rtsp" exec="image2rtsp" name="image2rtsp">
|
||||||
|
<env name="GST_DEBUG" value=""/> <!-- Setting to 3 can help if its not working -->
|
||||||
|
<param from="$(find-pkg-share am_i_up)/params/j7s_threed_image2rtsp.yaml"/>
|
||||||
|
</node>
|
||||||
|
|
||||||
<!-- Vehicle Position -->
|
<!-- Vehicle Position -->
|
||||||
<node pkg="data_simulator" exec="position_publisher" name="position_publisher" />
|
<node pkg="data_simulator" exec="position_publisher" name="position_publisher" />
|
||||||
|
|
||||||
|
<!-- Identity -->
|
||||||
|
<node pkg="data_simulator" exec="identity_publisher" name="identity_publisher" />
|
||||||
|
|
||||||
<!-- Diagnostics -->
|
<!-- Diagnostics -->
|
||||||
<node pkg="data_simulator" exec="diagnostic_publisher" name="diagnostic_1">
|
<node pkg="data_simulator" exec="diagnostic_publisher" name="diagnostic_1">
|
||||||
<param name="diag_level" value="OK"/>
|
<param name="diag_level" value="OK"/>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<depend>python3-aiohttp</depend>
|
<depend>python3-aiohttp</depend>
|
||||||
<!-- TODO: Run time dep on ping -->
|
<!-- TODO: Run time dep on ping -->
|
||||||
|
|
||||||
|
<exec_depend>j7s_diagnostics_py</exec_depend>
|
||||||
<exec_depend>rclpy</exec_depend>
|
<exec_depend>rclpy</exec_depend>
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,8 @@
|
|||||||
/**:
|
/**:
|
||||||
ros__parameters:
|
ros__parameters:
|
||||||
ui_pkg: "am_i_up_ui"
|
ui_pkg: "am_i_up_ui"
|
||||||
listen_port: "8000"
|
listen_port: "8000"
|
||||||
|
video_display_port: "8889"
|
||||||
|
video_streams:
|
||||||
|
- image
|
||||||
|
- threed_image
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**:
|
||||||
|
ros__parameters:
|
||||||
|
topic: "threed_image"
|
||||||
|
mountpoint: "/threed_image"
|
||||||
|
port: "8560"
|
||||||
|
local_only: True
|
||||||
|
default_pipeline: |
|
||||||
|
( appsrc name=imagesrc do-timestamp=true min-latency=0 max-latency=0 max-bytes=1000 is-live=true !
|
||||||
|
videoconvert !
|
||||||
|
videoscale !
|
||||||
|
video/x-raw, framerate=30/1, width=1280, height=720 !
|
||||||
|
x264enc tune=zerolatency bitrate=500 key-int-max=30 !
|
||||||
|
video/x-h264, profile=baseline !
|
||||||
|
rtph264pay name=pay0 pt=96 )
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
authMethod: http
|
||||||
|
authHTTPAddress: http://127.0.0.1:8000/api/mediamtx/auth
|
||||||
|
authHTTPExclude: []
|
||||||
paths:
|
paths:
|
||||||
image:
|
image:
|
||||||
source: rtsp://127.0.0.1:8559/image
|
source: rtsp://127.0.0.1:8559/image
|
||||||
|
threed_image:
|
||||||
|
source: rtsp://127.0.0.1:8560/threed_image
|
||||||
Reference in new issue
Block a user