Compare commits

...
21 Commits
Author SHA1 Message Date
jimmy 7f9ca762be Add identiy and version number. Some generic cleanup. 2026-09-16 19:51:01 -04:00
jimmy 991a44b332 Support multiple videos. 2026-09-12 13:17:55 +00:00
jimmy 2d5e570625 Fix login pagename. 2026-08-27 08:46:50 -04:00
jimmy 28fc4e0f68 Make a separate login page and protect assets. 2026-08-26 12:06:40 -04:00
jimmy 01523517d2 Require login for video streams. 2026-08-26 14:16:58 +00:00
jimmy 52d2aa52e2 First pass login support. 2026-08-22 10:42:33 -04:00
jimmy f51b1cf4c0 Refactor. Add in diagnostics. 2026-08-14 14:01:29 -04:00
jimmy 4bef10997a Use my aggregator. 2026-08-13 06:44:17 -04:00
jimmy 1d8bac9f7c Refactor launch files. Fake diagnostics. 2026-06-13 13:34:49 -04:00
jimmy a5f2c9952f Lyrical 2026-06-12 22:05:31 -04:00
jimmy 137fb39ef9 Cleanup deps. Add params. Setup mediamts and image2rtsp. 2026-05-26 14:53:52 -04:00
jimmy 0113cd4a12 Launch position simulator. 2026-03-07 16:31:27 -05:00
jimmy b2e862d4ac Refactor to make Facts public functions easier to understand. 2026-03-07 16:30:34 -05:00
jimmy 67bbd8f1b9 Get lat long. 2026-03-07 16:07:33 -05:00
jimmy 7b189e5177 Subscribe and show costmap images. 2026-03-02 12:34:58 +00:00
jimmy 6ade8fb818 Refactor server. 2026-03-01 18:34:50 -05:00
jimmy 85ed0af7e4 Play nice with react router. 2026-03-01 15:43:46 -05:00
jimmy 92f58959be Host the UI. 2026-03-01 15:26:19 -05:00
jimmy 2f64b53543 Print where I'm hosting from. 2026-03-01 13:33:41 -05:00
jimmy d1d0a61569 integrate ros. remove front end. 2026-02-28 10:54:57 -05:00
jimmy f0574de8f4 mutliple changes left from the last week. 2026-01-01 10:29:05 -05:00
19 changed files with 19068 additions and 191 deletions

No files matched your search

+8 -7
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 4.2)
project(am_i_up) project(am_i_up)
find_package(ament_cmake REQUIRED) find_package(ament_cmake REQUIRED)
@@ -6,15 +6,16 @@ find_package(ament_cmake_python REQUIRED)
ament_python_install_package(${PROJECT_NAME}) ament_python_install_package(${PROJECT_NAME})
install(DIRECTORY
front_end
DESTINATION share/${PROJECT_NAME}/
)
install(DIRECTORY install(DIRECTORY
scripts/ scripts/
DESTINATION lib/${PROJECT_NAME}/lib DESTINATION lib/${PROJECT_NAME}/
USE_SOURCE_PERMISSIONS USE_SOURCE_PERMISSIONS
) )
# Install launch and param files.
install(DIRECTORY
launch params assets
DESTINATION share/${PROJECT_NAME}/
)
ament_package() ament_package()
+197
View File
@@ -0,0 +1,197 @@
#
# 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.
#
from aiohttp import web
import asyncio
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
class Api:
def __init__(self, facts):
self._facts = facts
self._authenticator = Authenticator(facts.get_password(), self.get_open_endpoints())
self._mediamtx_authenticator = MediaMTXAuthenticator()
async def run(self):
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
ui_static_directory = ui_share_directory + "/dist/assets"
server_static_directory = get_package_share_directory("am_i_up") + "/assets"
app = web.Application(middlewares=[self._authenticator.build_middleware()])
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/version_number', self.version_number),
web.get('/api/env', self.env),
web.get('/api/identity', self.identity),
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("/server_assets", server_static_directory),
web.get('/login', self.login_page),
# we're not actually using key anywhere, but doing this allows react router
# to work correctly.
web.get("/{key}", self.index),
web.get("/", self.index)
])
url = "0.0.0.0"
port = self._facts.get_listen_port()
print("Listening on {}:{}".format(url, port))
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, url, port)
await site.start()
while rclpy.ok():
await asyncio.sleep(3600)
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):
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
ui_index_path = ui_share_directory + "/dist/index.html"
return web.FileResponse(ui_index_path)
async def ping(self, request):
request_dict = await request.json()
result = self._facts.do_ping(request_dict['address'])
resp = {"message": result}
return web.json_response(resp)
async def uptime(self, request):
resp = {"uptime": self._facts.get_uptime()}
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):
project_state = self._facts.get_buildinfo()
if not project_state:
resp = {"status": False, "message": "project_state not found."}
return web.json_response(resp)
resp = {
"status": True,
"message": project_state
}
return web.json_response(resp)
async def env(self, request):
env = self._facts.get_env()
return web.json_response(env)
async def identity(self, request):
identity = self._facts.get_identity()
if not identity:
identity = "Nothing received!"
resp = {"message": identity}
return web.json_response(resp)
async def position(self, request):
resp = {}
(lat, lon) = self._facts.get_lat_long()
if not lat or not lon:
resp["status"] = False
return web.json_response(resp)
resp["status"] = True
resp["latitude"] = lat
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)
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)
+47
View File
@@ -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
+207
View File
@@ -0,0 +1,207 @@
#
# 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 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
class Facts:
def __init__(self):
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()
async def run(self):
while rclpy.ok():
rclpy.spin_once(self._node, timeout_sec=0)
await asyncio.sleep(1e-4)
def get_lat_long(self):
return self._lat_long
def get_identity(self):
return self._identity_string
def get_listen_port(self):
return self._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)
def get_uptime(self):
return time.monotonic() - self._start_time
def do_ping(self, address):
# Make sure address is a ip address.
if not is_valid_ip(address):
return "Provied address ({}) is not valid.".format(address)
# Build command.
command = ["ping", "-W", "1", "-c", "1", address]
process = subprocess.run(command)
if process.returncode == 0:
return "{} responded to ping.".format(address)
return "{} did not respond to ping.".format(address)
def get_env(self):
env = {}
# We're not going to return the whole environment because
# of security.
# Let's pick the ones we want.
env['ROS_AUTOMATIC_DISCOVERY_RANGE'] = os.environ.get('ROS_AUTOMATIC_DISCOVERY_RANGE', None)
env['AMENT_PREFIX_PATH'] = os.environ.get('AMENT_PREFIX_PATH', None)
env['ROS_DISTRO'] = os.environ.get('ROS_DISTRO', None)
env['RMW_IMPLEMENTATION'] = os.environ.get('RMW_IMPLEMENTATION', None)
env['ROS_NAMESPACE'] = os.environ.get('ROS_NAMESPACE', None)
env['CYCLONEDDS_URI'] = os.environ.get('CYCLONEDDS_URI', None)
return env
def get_buildinfo(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 project_state.repos file in it.
project_state_file = build_info_getter_directory + "/project_state.repos"
project_state_content = None
try:
with open(project_state_file, 'r') as file_obj:
project_state_content = yaml.safe_load(file_obj)
except Exception as e:
# We either didn't load the file or couldn't read it
# as json.
print("Can't find build info.\n{}".format(e))
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):
try:
ipaddress.ip_address(address)
return True
except ValueError:
return False
+25
View File
@@ -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
+66 -22
View File
@@ -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,25 +16,40 @@ 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):
request = { "password": "password" }
async with session.post(f'{self._host}/api/login', json=request) as resp:
if not resp.ok:
print("Login failed.")
async def call_ping(self, options):
if len(options) != 1 or options[0] == "": if len(options) != 1 or options[0] == "":
raise RunTimeError("Ping option is an address as a string.") raise RunTimeError("Ping option is an address as a string.")
@@ -42,26 +58,54 @@ async def call_ping(options):
print("Calling ping with request: {}", json.dumps(request)) print("Calling ping with request: {}", json.dumps(request))
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('http://localhost:8080/ping', json=request) as resp: await self.login(session)
async with session.get(f'{self._host}/api/ping', json=request) as resp:
print(await resp.text()) print(await resp.text())
async def call_uptime(): async def call_uptime(self):
print("Calling uptime.") print("Calling uptime.")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('http://localhost:8080/uptime') as resp: await self.login(session)
async with session.get(f'{self._host}/api/uptime') as resp:
print(await resp.text()) print(await resp.text())
async def call_build_info(): async def call_build_info(self):
print("Calling build_info.") print("Calling build_info.")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('http://localhost:8080/build_info') as resp: await self.login(session)
async with session.get(f'{self._host}/api/build_info') as resp:
print(await resp.text()) print(await resp.text())
async def call_env(): async def call_env(self):
print("Calling env.") print("Calling env.")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('http://localhost:8080/env') as resp: 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()) print(await resp.text())
+8 -129
View File
@@ -8,137 +8,16 @@
# 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.
# #
from aiohttp import web from am_i_up.Api import Api
import time from am_i_up.Facts import Facts
from ament_index_python import get_package_share_directory
import yaml
import ipaddress
import subprocess
import os
from jinja2 import Environment, FileSystemLoader, select_autoescape
import asyncio
def main(): async def main():
facts = Facts() facts = Facts()
view = View() api = Api(facts)
routes = Routes(facts, view)
app = web.Application() future = asyncio.gather(api.run(), facts.run())
app.add_routes([ await future
web.get('/', routes.root),
web.get('/ping', routes.ping),
web.get('/uptime', routes.uptime),
web.get('/build_info', routes.build_info),
web.get('/env', routes.env)
])
web.run_app(app)
class View: asyncio.run(main())
def __init__(self):
# TODO: Make global for the script directory.
self._env = Environment(
loader=FileSystemLoader(self._find_template_dirs()),
autoescape=select_autoescape()
)
def render_root(self):
return self._env.get_template('base.html').render()
def _find_template_dirs(self):
package_dir = get_package_share_directory('am_i_up')
template_dir = "{}/front_end/templates".format(package_dir)
if not os.path.exists(template_dir):
raise RuntimeError("Could not find template_dir: {}".format(template_dir))
return [template_dir]
class Facts:
def __init__(self):
self._start_time = time.monotonic()
def get_uptime(self):
return time.monotonic() - self._start_time
def do_ping(self, address):
# Make sure address is a ip address.
if not is_valid_ip(address):
return "Provied address ({}) is not valid.".format(address)
# Build command.
command = ["ping", "-W", "1", "-c", "1", address]
process = subprocess.run(command)
if process.returncode == 0:
return "{} responded to ping.".format(address)
return "{} did not respond to ping.".format(address)
def get_env(self):
env = {}
# We're not going to return the whole environment because
# of security.
# Let's pick the ones we want.
env['ROS_AUTOMATIC_DISCOVERY_RANGE'] = os.environ.get('ROS_AUTOMATIC_DISCOVERY_RANGE', None)
env['AMENT_PREFIX_PATH'] = os.environ.get('AMENT_PREFIX_PATH', None)
env['ROS_DISTRO'] = os.environ.get('ROS_DISTRO', None)
env['RMW_IMPLEMENTATION'] = os.environ.get('RMW_IMPLEMENTATION', None)
env['ROS_NAMESPACE'] = os.environ.get('ROS_NAMESPACE', None)
env['CYCLONEDDS_URI'] = os.environ.get('CYCLONEDDS_URI', None)
return env
def get_buildinfo(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 project_state.repos file in it.
project_state_file = build_info_getter_directory + "/project_state.repos"
project_state_content = None
try:
with open(project_state_file, 'r') as file_obj:
project_state_content = yaml.safe_load(file_obj)
except Exception as e:
# We either didn't load the file or couldn't read it
# as json.
print("Can't find build info.\n{}".format(e))
return project_state_content
class Routes:
def __init__(self, facts, view):
self._facts = facts
self._view = view
async def root(self, request):
text = self._view.render_root()
return web.Response(text=text, content_type='text/html')
async def ping(self, request):
request_dict = await request.json()
result = self._facts.do_ping(request_dict['address'])
resp = {"message": result}
return web.json_response(resp)
async def uptime(self, request):
resp = {"uptime": self._facts.get_uptime()}
return web.json_response(resp)
async def build_info(self, request):
project_state = self._facts.get_buildinfo()
if not project_state:
resp = {"status": False, "message": "project_state not found."}
return web.json_response(resp)
project_state["status"] = True
return web.json_response(project_state)
async def env(self, request):
env = self._facts.get_env()
return web.json_response(env)
def is_valid_ip(address):
try:
ipaddress.ip_address(address)
return True
except ValueError:
return False
+6312
View File
File diff suppressed because it is too large. Load diff
+12048
View File
File diff suppressed because it is too large. Load diff
+54
View File
@@ -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>
-32
View File
@@ -1,32 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>{% block title %}Robot Status UI{% endblock %}</title>
</head>
<body>
<!-- NavBar -->
<!-- Content -->
<div class="container">
{% block content %}
<div>Hello World!</div>
{% endblock %}
</div>
<!-- Load JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
{% block js %}{% endblock %}
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<launch>
<node pkg="am_i_up" exec="server" name="am_i_up">
<param from="$(find-pkg-share am_i_up)/params/am_i_up.yaml"/>
</node>
<include file="$(find-pkg-share media_mtx_wrapper)/launch/mediamtx.launch.xml">
<arg name="config_file_path" value="$(find-pkg-share am_i_up)/params/mediamtx.yaml" />
</include>
<include file="$(find-pkg-share am_i_up)/launch/data_simulators.launch.xml" />
</launch>
+39
View File
@@ -0,0 +1,39 @@
<launch>
<!-- Images -->
<node pkg="data_simulator" exec="image_publisher" name="image_publisher" />
<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/image2rtsp.yaml"/>
</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 -->
<node pkg="data_simulator" exec="position_publisher" name="position_publisher" />
<!-- Identity -->
<node pkg="data_simulator" exec="identity_publisher" name="identity_publisher" />
<!-- Diagnostics -->
<node pkg="data_simulator" exec="diagnostic_publisher" name="diagnostic_1">
<param name="diag_level" value="OK"/>
<param name="node_name" value="node_1"/>
</node>
<node pkg="data_simulator" exec="diagnostic_publisher" name="diagnostic_2">
<param name="diag_level" value="WARN"/>
<param name="node_name" value="node_2"/>
</node>
<!-- Aggregator -->
<node pkg="j7s_diagnostics_ros" exec="j7s_diagnostic_node"/>
</launch>
+1 -1
View File
@@ -10,9 +10,9 @@
<buildtool_depend>ament_cmake_python</buildtool_depend> <buildtool_depend>ament_cmake_python</buildtool_depend>
<depend>python3-aiohttp</depend> <depend>python3-aiohttp</depend>
<depend>python3-jinja2</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>
+8
View File
@@ -0,0 +1,8 @@
/**:
ros__parameters:
ui_pkg: "am_i_up_ui"
listen_port: "8000"
video_display_port: "8889"
video_streams:
- image
- threed_image
+7
View File
@@ -0,0 +1,7 @@
/**:
ros__parameters:
analyzers:
all:
type: 'diagnostic_aggregator/GenericAnalyzer'
path: 'Agg'
regex: ['.*']
+7
View File
@@ -0,0 +1,7 @@
/**:
ros__parameters:
compressed: True
topic: "image/compressed"
mountpoint: "/image"
port: "8559"
local_only: True
+14
View File
@@ -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 )
+8
View File
@@ -0,0 +1,8 @@
authMethod: http
authHTTPAddress: http://127.0.0.1:8000/api/mediamtx/auth
authHTTPExclude: []
paths:
image:
source: rtsp://127.0.0.1:8559/image
threed_image:
source: rtsp://127.0.0.1:8560/threed_image