Compare commits
5
Commits
52d2aa52e2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f9ca762be | ||
|
|
991a44b332 | ||
|
|
2d5e570625 | ||
|
|
28fc4e0f68 | ||
|
|
01523517d2 |
No files matched your search
+1
-1
@@ -14,7 +14,7 @@ install(DIRECTORY
|
||||
|
||||
# Install launch and param files.
|
||||
install(DIRECTORY
|
||||
launch params
|
||||
launch params assets
|
||||
DESTINATION share/${PROJECT_NAME}/
|
||||
)
|
||||
|
||||
|
||||
+75
-8
@@ -12,32 +12,43 @@ 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(), facts.get_open_endpoints())
|
||||
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/status', self.status),
|
||||
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("/{key}", self.index),
|
||||
web.get("/", self.index)
|
||||
])
|
||||
|
||||
url = "0.0.0.0"
|
||||
@@ -53,6 +64,45 @@ class Api:
|
||||
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()
|
||||
@@ -85,6 +135,17 @@ class Api:
|
||||
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:
|
||||
@@ -100,11 +161,11 @@ class Api:
|
||||
env = self._facts.get_env()
|
||||
return web.json_response(env)
|
||||
|
||||
async def status(self, request):
|
||||
status = self._facts.get_status()
|
||||
if not status:
|
||||
status = "Nothing received!"
|
||||
resp = {"message": status}
|
||||
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):
|
||||
@@ -128,3 +189,9 @@ class Api:
|
||||
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)
|
||||
|
||||
+34
-9
@@ -30,7 +30,7 @@ class Facts:
|
||||
rclpy.init()
|
||||
self._node = rclpy.create_node('am_i_up')
|
||||
|
||||
self._status_sub = self._node.create_subscription(String, "status", self._status_callback, 1)
|
||||
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)
|
||||
|
||||
@@ -39,7 +39,10 @@ class Facts:
|
||||
|
||||
self._password = self._node.declare_parameter("password", "password").value
|
||||
|
||||
self._status_string = None
|
||||
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
|
||||
|
||||
@@ -53,8 +56,8 @@ class Facts:
|
||||
def get_lat_long(self):
|
||||
return self._lat_long
|
||||
|
||||
def get_status(self):
|
||||
return self._status_string
|
||||
def get_identity(self):
|
||||
return self._identity_string
|
||||
|
||||
def get_listen_port(self):
|
||||
return self._listen_port
|
||||
@@ -116,15 +119,37 @@ class Facts:
|
||||
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_open_endpoints(self):
|
||||
# Hardcoding for now, can make a parameter later.
|
||||
return ["/login", "/api/login", "/assets/(.*)"]
|
||||
def get_video_streams(self):
|
||||
return self._video_streams
|
||||
|
||||
def _status_callback(self, msg):
|
||||
self._status_string = msg.data
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+20
-1
@@ -22,7 +22,6 @@ def main(args=None):
|
||||
parser.add_argument("--host", default="http://localhost:8000")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
client = Client(args.host)
|
||||
|
||||
if args.action == 'ping':
|
||||
@@ -35,6 +34,10 @@ def main(args=None):
|
||||
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())
|
||||
|
||||
class Client():
|
||||
def __init__(self, host):
|
||||
@@ -83,6 +86,22 @@ class Client():
|
||||
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.")
|
||||
|
||||
|
||||
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"/>
|
||||
</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"/>
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
ros__parameters:
|
||||
ui_pkg: "am_i_up_ui"
|
||||
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:
|
||||
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