Phase 3 Stage 2: rov_api FastAPI backend (core)
- New rov_api package: HTTP-to-ROS2 bridge on port 8081 - GET /health: mission state, recording active, failsafe state - POST /abort: W3 RETURN TO SAFE, publishes Bool to /rov/mission/abort - POST /mission/start: MissionCommand START (recording no-go gate enforced) - POST /mission/stop: MissionCommand ABORT - Embedded rclpy node in background thread, uvicorn in main thread - Verified end-to-end: /abort -> failsafe_monitor FSM NORMAL -> HOLD_AND_RECOVER - Runs as argonaut-api.service (manual-start in dev, same policy as argonaut.service) - Deferred to Stage 2b: /return_budget, /backup/*, /mission/upload
This commit is contained in:
parent
06403533f2
commit
66a18ef707
28
src/rov_api/package.xml
Normal file
28
src/rov_api/package.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>rov_api</name>
|
||||
<version>0.1.0</version>
|
||||
<description>
|
||||
Argonaut 3 FastAPI backend — HTTP bridge from Cockpit widgets to the
|
||||
ROS2 autonomy stack. Provides /health, /abort (W3 RETURN TO SAFE),
|
||||
/mission/start, and /mission/stop endpoints on port 8081.
|
||||
</description>
|
||||
<maintainer email="grant@symbytech.com">Grant du Toit</maintainer>
|
||||
<license>Proprietary</license>
|
||||
|
||||
<!-- Build tooling for a Python ament package -->
|
||||
<buildtool_depend>ament_python</buildtool_depend>
|
||||
|
||||
<!-- ROS2 runtime dependencies -->
|
||||
<exec_depend>rclpy</exec_depend>
|
||||
<exec_depend>std_msgs</exec_depend>
|
||||
<exec_depend>rov_interfaces</exec_depend>
|
||||
|
||||
<!-- FastAPI/uvicorn are pip-installed system-wide (not rosdep keys);
|
||||
documented in the deploy notes, not declared here as ament deps. -->
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
0
src/rov_api/resource/rov_api
Normal file
0
src/rov_api/resource/rov_api
Normal file
0
src/rov_api/rov_api/__init__.py
Normal file
0
src/rov_api/rov_api/__init__.py
Normal file
488
src/rov_api/rov_api/api_node.py
Executable file
488
src/rov_api/rov_api/api_node.py
Executable file
@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
api_node.py — Argonaut 3 FastAPI Backend (Phase 3 Stage 2, core).
|
||||
|
||||
Bridges operator-facing HTTP requests (from Cockpit DIY widgets) to the
|
||||
ROS2 autonomy stack. Runs FastAPI/uvicorn in the main thread and an
|
||||
embedded rclpy node in a background thread; the two communicate through
|
||||
the RovApiNode instance held in shared module state.
|
||||
|
||||
ARCHITECTURE
|
||||
------------
|
||||
Cockpit widget (fetch) --HTTP--> FastAPI (uvicorn, main thread)
|
||||
|
|
||||
v
|
||||
RovApiNode (rclpy, bg thread)
|
||||
|
|
||||
+-------------------------+--------------------------+
|
||||
| | |
|
||||
publish /rov/mission/abort call /rov/mission/command cache latest:
|
||||
(Bool, W3 RETURN TO SAFE) (MissionCommand srv) /rov/mission/status
|
||||
START / ABORT /rov/recording/active
|
||||
/rov/failsafe
|
||||
|
||||
WHY THIS DESIGN
|
||||
---------------
|
||||
FastAPI is NOT a ROS2 node. rclpy needs its own executor spinning to
|
||||
service subscriptions and service clients. We therefore run rclpy.spin()
|
||||
in a daemon background thread, and let the FastAPI request handlers call
|
||||
thread-safe methods on the shared RovApiNode. rclpy publishers, service
|
||||
clients, and subscription callbacks are safe to touch from another thread
|
||||
as long as we do not call rclpy.spin() more than once.
|
||||
|
||||
ENDPOINTS (core only — Stage 2b adds /return_budget, /backup/*, /mission/upload)
|
||||
-------------------------------------------------------------------------------
|
||||
GET /health -> mission state + recording active + failsafe state
|
||||
POST /abort -> W3 RETURN TO SAFE (publishes Bool true to /rov/mission/abort)
|
||||
POST /mission/start -> MissionCommand START
|
||||
POST /mission/stop -> MissionCommand ABORT
|
||||
|
||||
VERIFIED INTERFACES (read from source before writing — do not change without re-checking)
|
||||
-----------------------------------------------------------------------------------------
|
||||
/rov/mission/abort : std_msgs/Bool
|
||||
failsafe_monitor._abort_callback sets flag_manual_abort=True
|
||||
when msg.data is True. This is the vehicle-layer return-to-safe
|
||||
path (DIR-5), correct for W3.
|
||||
/rov/mission/command: rov_interfaces/srv/MissionCommand
|
||||
Request : string command, string mission_id, string[] parameters
|
||||
Response: bool success, string message
|
||||
Valid commands (mission_executor): START, LOAD, PAUSE, RESUME, ABORT
|
||||
/rov/mission/status : rov_interfaces/msg/MissionStatus (state, progress_percent, ...)
|
||||
/rov/recording/active: std_msgs/Bool (recording_manager no-go gate)
|
||||
/rov/failsafe : rov_interfaces/msg/FailsafeStatus (failsafe_state, assessment_state, ...)
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
# ROS2
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from std_msgs.msg import Bool
|
||||
from rov_interfaces.msg import MissionStatus, FailsafeStatus
|
||||
from rov_interfaces.srv import MissionCommand
|
||||
|
||||
# FastAPI
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Port the FastAPI backend listens on. Matches the W3/W4 widget config
|
||||
# constant FASTAPI_HOST = http://192.168.1.101:8081.
|
||||
API_PORT = 8081
|
||||
|
||||
# Bind on all interfaces so Cockpit (on the laptop, over the tether/LAN)
|
||||
# can reach the API. UFW is deferred to the pre-field hardening pass;
|
||||
# until then there is no external exposure (Tailscale disabled, no proxy).
|
||||
API_HOST = '0.0.0.0'
|
||||
|
||||
# Timeout (seconds) to wait for the mission_executor service to become
|
||||
# available before failing a mission command request. Kept short so the
|
||||
# widget gets a fast, clear error rather than hanging.
|
||||
SERVICE_WAIT_TIMEOUT_S = 2.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic request models
|
||||
# These define and validate the JSON body shape for POST endpoints.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MissionStartRequest(BaseModel):
|
||||
"""
|
||||
Body for POST /mission/start.
|
||||
|
||||
mission_id : optional identifier echoed back in status. Defaults to
|
||||
empty string if the widget does not supply one.
|
||||
parameters : optional list of strings passed through to the mission
|
||||
executor (e.g. operation mode 'tethered'/'untethered').
|
||||
The mission plan itself is loaded separately (DIR-3:
|
||||
mission is uploaded beforehand, not at button-time).
|
||||
"""
|
||||
mission_id: str = ''
|
||||
parameters: list[str] = []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ROS2 node
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class RovApiNode(Node):
|
||||
"""
|
||||
Embedded ROS2 node backing the FastAPI endpoints.
|
||||
|
||||
Owns:
|
||||
- a publisher on /rov/mission/abort (Bool) for W3 RETURN TO SAFE
|
||||
- a service client on /rov/mission/command for mission START/STOP
|
||||
- subscriptions caching the latest mission status, recording state,
|
||||
and failsafe state, used to answer GET /health without blocking
|
||||
|
||||
All cached values are plain Python types updated inside subscription
|
||||
callbacks. Reads from the FastAPI thread are atomic (single attribute
|
||||
reads of immutable values) so no explicit lock is required for them.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('rov_api')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Cached latest values for /health
|
||||
# Initialised to sentinels indicating "no data received yet" so the
|
||||
# health endpoint can report honestly before the stack publishes.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# Latest mission state integer (MissionStatus.STATE_*); -1 = no data
|
||||
self._mission_state: int = -1
|
||||
|
||||
# Latest mission progress percent (0-100); 0 until data arrives
|
||||
self._mission_progress: float = 0.0
|
||||
|
||||
# Latest recording-active flag; None = no data, True/False once known
|
||||
self._recording_active: Optional[bool] = None
|
||||
|
||||
# Latest failsafe state integer (FailsafeStatus.*); -1 = no data
|
||||
self._failsafe_state: int = -1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Publisher — /rov/mission/abort (W3 RETURN TO SAFE)
|
||||
# Publishing Bool(true) sets flag_manual_abort in failsafe_monitor,
|
||||
# which drives the vehicle-layer return-to-safe action (DIR-5).
|
||||
# ------------------------------------------------------------------
|
||||
self._abort_pub = self.create_publisher(
|
||||
Bool, '/rov/mission/abort', 10)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Service client — /rov/mission/command
|
||||
# Used for mission START and STOP. mission_executor enforces the
|
||||
# recording no-go gate (DIR-9) on START, so the API does not need
|
||||
# to re-check recording here — a blocked START returns success=False
|
||||
# with an explanatory message which we pass straight back to the widget.
|
||||
# ------------------------------------------------------------------
|
||||
self._mission_cli = self.create_client(
|
||||
MissionCommand, '/rov/mission/command')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Subscriptions — cache latest values for /health
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# Mission status from mission_executor (2 Hz)
|
||||
self.create_subscription(
|
||||
MissionStatus, '/rov/mission/status',
|
||||
self._mission_status_cb, 10)
|
||||
|
||||
# Recording active from recording_manager (1 Hz) — no-go gate signal
|
||||
self.create_subscription(
|
||||
Bool, '/rov/recording/active',
|
||||
self._recording_cb, 10)
|
||||
|
||||
# Failsafe status from failsafe_monitor (rate varies with state)
|
||||
self.create_subscription(
|
||||
FailsafeStatus, '/rov/failsafe',
|
||||
self._failsafe_cb, 10)
|
||||
|
||||
self.get_logger().info('RovApiNode started — HTTP bridge on :%d' % API_PORT)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Subscription callbacks — update cached values
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _mission_status_cb(self, msg: MissionStatus):
|
||||
"""Cache the latest mission state and progress for /health."""
|
||||
self._mission_state = int(msg.state)
|
||||
self._mission_progress = float(msg.progress_percent)
|
||||
|
||||
def _recording_cb(self, msg: Bool):
|
||||
"""Cache the latest recording-active flag for /health."""
|
||||
self._recording_active = bool(msg.data)
|
||||
|
||||
def _failsafe_cb(self, msg: FailsafeStatus):
|
||||
"""Cache the latest failsafe state for /health."""
|
||||
self._failsafe_state = int(msg.failsafe_state)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions called from the FastAPI thread
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def publish_abort(self):
|
||||
"""
|
||||
Publish Bool(true) to /rov/mission/abort — W3 RETURN TO SAFE.
|
||||
|
||||
This is fire-and-forget: the failsafe monitor latches
|
||||
flag_manual_abort on the rising edge, so a single publish is
|
||||
sufficient. Called from the FastAPI request thread; create_publisher
|
||||
publish is thread-safe with respect to the spinning executor.
|
||||
"""
|
||||
msg = Bool()
|
||||
msg.data = True
|
||||
self._abort_pub.publish(msg)
|
||||
self.get_logger().warn('RETURN TO SAFE published to /rov/mission/abort (W3)')
|
||||
|
||||
def call_mission_command(self, command: str, mission_id: str = '',
|
||||
parameters: Optional[list] = None):
|
||||
"""
|
||||
Call the /rov/mission/command service synchronously and return
|
||||
(success: bool, message: str).
|
||||
|
||||
Because we are calling from the FastAPI thread (NOT the executor
|
||||
thread), we cannot use the spin_until_future_complete pattern —
|
||||
that would require spinning the executor, which is already being
|
||||
spun in the background thread. Instead we use the blocking
|
||||
call() convenience via a threading.Event on the future's callback.
|
||||
|
||||
command : one of START / ABORT / PAUSE / RESUME / LOAD
|
||||
mission_id : optional identifier
|
||||
parameters : optional list of strings
|
||||
"""
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
|
||||
# Ensure the mission_executor service is actually up. If the stack
|
||||
# is not running, fail fast with a clear message rather than hang.
|
||||
if not self._mission_cli.wait_for_service(timeout_sec=SERVICE_WAIT_TIMEOUT_S):
|
||||
return False, (
|
||||
'mission_executor service /rov/mission/command unavailable — '
|
||||
'is argonaut.service running?'
|
||||
)
|
||||
|
||||
# Build the request per the verified .srv definition:
|
||||
# string command, string mission_id, string[] parameters
|
||||
req = MissionCommand.Request()
|
||||
req.command = command
|
||||
req.mission_id = mission_id
|
||||
req.parameters = parameters
|
||||
|
||||
# Fire the async request. We then block this (FastAPI) thread on a
|
||||
# threading.Event that the future's done-callback sets. The future
|
||||
# is completed by the background executor thread, so this is the
|
||||
# correct cross-thread pattern (do NOT spin here).
|
||||
future = self._mission_cli.call_async(req)
|
||||
done_event = threading.Event()
|
||||
|
||||
def _on_done(_fut):
|
||||
# Called by the executor thread when the response arrives.
|
||||
done_event.set()
|
||||
|
||||
future.add_done_callback(_on_done)
|
||||
|
||||
# Wait for the response, bounded by a timeout so a stuck service
|
||||
# cannot hang the HTTP request indefinitely.
|
||||
if not done_event.wait(timeout=SERVICE_WAIT_TIMEOUT_S + 3.0):
|
||||
return False, 'mission command timed out waiting for response'
|
||||
|
||||
# Retrieve the result. future.result() is safe now the callback fired.
|
||||
result = future.result()
|
||||
if result is None:
|
||||
return False, 'mission command failed — no response from service'
|
||||
|
||||
return bool(result.success), str(result.message)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Health snapshot for GET /health
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def health_snapshot(self) -> dict:
|
||||
"""
|
||||
Return a plain dict of the latest cached stack state.
|
||||
|
||||
Values are read atomically (single immutable attribute reads) so
|
||||
this is safe to call from the FastAPI thread without a lock.
|
||||
"""
|
||||
return {
|
||||
'mission_state': self._mission_state, # int, -1 = no data
|
||||
'mission_progress': self._mission_progress, # float 0-100
|
||||
'recording_active': self._recording_active, # bool or None
|
||||
'failsafe_state': self._failsafe_state, # int, -1 = no data
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared node handle
|
||||
# Populated by main() before uvicorn starts; read by endpoint handlers.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Module-level reference to the single RovApiNode instance. Set in main().
|
||||
# FastAPI endpoint functions access this to reach ROS2. It is created and
|
||||
# assigned before uvicorn.run() is called, so it is never None during requests.
|
||||
_node: Optional[RovApiNode] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FastAPI application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
app = FastAPI(
|
||||
title='Argonaut 3 API',
|
||||
description='HTTP bridge from Cockpit widgets to the ROS2 autonomy stack.',
|
||||
version='1.0.0',
|
||||
)
|
||||
|
||||
# CORS — Cockpit widgets run in the Cockpit app and POST cross-origin.
|
||||
# Allow all origins for LAN/dev use. Tighten to the Cockpit origin at the
|
||||
# pre-field hardening pass alongside UFW and the SSH password change.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'], # dev: any origin; tighten before field
|
||||
allow_credentials=False,
|
||||
allow_methods=['*'],
|
||||
allow_headers=['*'],
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /health
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@app.get('/health')
|
||||
def health():
|
||||
"""
|
||||
Return the current stack state snapshot.
|
||||
|
||||
Used by widgets and for quick manual checks (curl). Reports mission
|
||||
state, mission progress, recording-active flag, and failsafe state
|
||||
from the latest cached ROS2 messages. Sentinel values (-1 / None)
|
||||
indicate no data has been received on that topic yet.
|
||||
"""
|
||||
if _node is None:
|
||||
# Should never happen — node is created before uvicorn starts.
|
||||
return {'ok': False, 'error': 'ROS2 node not initialised'}
|
||||
|
||||
snap = _node.health_snapshot()
|
||||
return {
|
||||
'ok': True,
|
||||
'mission_state': snap['mission_state'],
|
||||
'mission_progress': snap['mission_progress'],
|
||||
'recording_active': snap['recording_active'],
|
||||
'failsafe_state': snap['failsafe_state'],
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /abort — W3 RETURN TO SAFE
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@app.post('/abort')
|
||||
def abort():
|
||||
"""
|
||||
W3 RETURN TO SAFE — vehicle-layer action (DIR-5).
|
||||
|
||||
Publishes Bool(true) to /rov/mission/abort, which the failsafe monitor
|
||||
latches as flag_manual_abort and drives the return-to-safe action.
|
||||
|
||||
This is deliberately NOT the mission ABORT service — the DIR-5 intent
|
||||
is a vehicle return-to-safe (follow breadcrumb path to the closest
|
||||
safe place), not merely ending mission execution.
|
||||
"""
|
||||
if _node is None:
|
||||
return {'ok': False, 'error': 'ROS2 node not initialised'}
|
||||
|
||||
_node.publish_abort()
|
||||
return {
|
||||
'ok': True,
|
||||
'action': 'return_to_safe',
|
||||
'message': 'RETURN TO SAFE command sent to vehicle.',
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /mission/start
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@app.post('/mission/start')
|
||||
def mission_start(req: MissionStartRequest):
|
||||
"""
|
||||
Start the loaded mission plan via the MissionCommand START service.
|
||||
|
||||
The mission plan must already be loaded (DIR-3: mission is uploaded
|
||||
beforehand). mission_executor enforces the recording no-go gate
|
||||
(DIR-9): if recording is not active, START returns success=False with
|
||||
an explanatory message, which we pass straight back to the widget.
|
||||
"""
|
||||
if _node is None:
|
||||
return {'ok': False, 'error': 'ROS2 node not initialised'}
|
||||
|
||||
success, message = _node.call_mission_command(
|
||||
command='START',
|
||||
mission_id=req.mission_id,
|
||||
parameters=req.parameters,
|
||||
)
|
||||
return {'ok': success, 'message': message}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /mission/stop
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@app.post('/mission/stop')
|
||||
def mission_stop():
|
||||
"""
|
||||
Stop the running mission via the MissionCommand ABORT service.
|
||||
|
||||
This is the mission-layer stop (DIR-4) — it ends mission execution and
|
||||
stops motion via mission_executor. It is distinct from W3 RETURN TO SAFE
|
||||
(/abort), which is the vehicle-layer return-to-safe action.
|
||||
|
||||
NOTE: mission_executor maps ABORT to STATE_ABORTED and stops motion.
|
||||
A dedicated mission STOP (recording stop + clean end) is part of a later
|
||||
stage; for now ABORT is the available clean-stop command.
|
||||
"""
|
||||
if _node is None:
|
||||
return {'ok': False, 'error': 'ROS2 node not initialised'}
|
||||
|
||||
success, message = _node.call_mission_command(command='ABORT')
|
||||
return {'ok': success, 'message': message}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main(args=None):
|
||||
"""
|
||||
Initialise ROS2, start the embedded node spinning in a background
|
||||
thread, then run uvicorn (FastAPI) in the main thread.
|
||||
|
||||
Shutdown: uvicorn.run blocks until the process is signalled. On exit
|
||||
we tear down rclpy cleanly. The background spin thread is a daemon so
|
||||
it does not block process exit.
|
||||
"""
|
||||
global _node
|
||||
|
||||
# Bring up ROS2 and create the bridge node BEFORE starting uvicorn,
|
||||
# so endpoint handlers always have a valid _node.
|
||||
rclpy.init(args=args)
|
||||
_node = RovApiNode()
|
||||
|
||||
# Spin the node in a daemon background thread. This services the
|
||||
# subscriptions (health cache) and the mission service client futures.
|
||||
def _spin():
|
||||
# rclpy.spin blocks, servicing callbacks, until shutdown.
|
||||
try:
|
||||
rclpy.spin(_node)
|
||||
except Exception:
|
||||
# On shutdown the context is torn down; swallow the resulting
|
||||
# exception so the daemon thread exits quietly.
|
||||
pass
|
||||
|
||||
spin_thread = threading.Thread(target=_spin, daemon=True)
|
||||
spin_thread.start()
|
||||
|
||||
# Run the HTTP server in the main thread. This blocks until the process
|
||||
# is stopped (Ctrl+C or systemd stop).
|
||||
try:
|
||||
uvicorn.run(app, host=API_HOST, port=API_PORT, log_level='info')
|
||||
finally:
|
||||
# Clean ROS2 shutdown. Guard against double-shutdown races.
|
||||
_node.destroy_node()
|
||||
try:
|
||||
rclpy.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
4
src/rov_api/setup.cfg
Normal file
4
src/rov_api/setup.cfg
Normal file
@ -0,0 +1,4 @@
|
||||
[develop]
|
||||
script_dir=$base/lib/rov_api
|
||||
[install]
|
||||
install_scripts=$base/lib/rov_api
|
||||
26
src/rov_api/setup.py
Normal file
26
src/rov_api/setup.py
Normal file
@ -0,0 +1,26 @@
|
||||
from setuptools import setup
|
||||
|
||||
package_name = 'rov_api'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.1.0',
|
||||
packages=[package_name],
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages',
|
||||
['resource/' + package_name]),
|
||||
('share/' + package_name, ['package.xml']),
|
||||
],
|
||||
install_requires=['setuptools'],
|
||||
zip_safe=True,
|
||||
maintainer='Grant du Toit',
|
||||
maintainer_email='grant@symbytech.com',
|
||||
description='Argonaut 3 FastAPI backend — HTTP bridge to the ROS2 autonomy stack.',
|
||||
license='Proprietary',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
# FastAPI backend node — HTTP bridge on :8081 for W3/W4 + mission control
|
||||
'api_node = rov_api.api_node:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user