|
1 | 1 | """ |
2 | 2 | Ground Control Station — FastAPI application entry point. |
3 | | -
|
4 | | -This is a minimal scaffold. Students should implement full functionality: |
| 3 | +========================================================= |
| 4 | +
|
| 5 | +This module is the heart of the backend. FastAPI reads this file when the |
| 6 | +server starts and uses the `app` object defined here to handle every incoming |
| 7 | +HTTP request. |
| 8 | +
|
| 9 | +Key concepts demonstrated in this file |
| 10 | +--------------------------------------- |
| 11 | +* **FastAPI application factory** — how to create and configure an `app`. |
| 12 | +* **CORS middleware** — what Cross-Origin Resource Sharing is and why it's |
| 13 | + needed when a browser frontend talks to a separate backend server. |
| 14 | +* **Environment variables** — the standard way to inject runtime configuration |
| 15 | + (URLs, log levels, secrets) without hard-coding values. |
| 16 | +* **Structured logging** — using Python's built-in `logging` module rather |
| 17 | + than plain `print()` calls, which is the industry standard. |
| 18 | +* **Async route handlers** — why `async def` matters for I/O-bound work like |
| 19 | + HTTP calls to a robot API. |
| 20 | +* **Error handling** — catching specific exceptions and returning meaningful |
| 21 | + responses instead of letting the server crash. |
| 22 | +
|
| 23 | +Running the server locally |
| 24 | +-------------------------- |
| 25 | +From the `backend/` directory: |
| 26 | +
|
| 27 | + uvicorn main:app --host 0.0.0.0 --port 8000 --reload |
| 28 | +
|
| 29 | +Then visit http://localhost:8000/docs for the interactive API documentation |
| 30 | +that FastAPI generates automatically from your code (no extra work required). |
5 | 31 | """ |
6 | 32 |
|
7 | 33 | import logging |
|
12 | 38 |
|
13 | 39 | from robot_client import robot, RobotConnectionError |
14 | 40 |
|
| 41 | +# ── Configuration from environment variables ─────────────────────────────── |
| 42 | +# os.getenv(key, default) reads a value from the process environment. |
| 43 | +# If the variable is not set, the second argument is used as a fallback. |
| 44 | +# |
| 45 | +# Why environment variables instead of hard-coded strings? |
| 46 | +# • Different environments (dev / staging / production) can use different |
| 47 | +# values without changing source code. |
| 48 | +# • Secrets (API keys, passwords) are never committed to version control. |
| 49 | +# • The values can be overridden in docker-compose.yml or a .env file |
| 50 | +# without touching this file. |
15 | 51 | ROBOT_API_URL = os.getenv("ROBOT_API_URL", "http://localhost:5000") |
16 | 52 | LOG_LEVEL = os.getenv("LOG_LEVEL", "info") |
17 | 53 |
|
| 54 | +# ── Logging setup ────────────────────────────────────────────────────────── |
| 55 | +# basicConfig() configures the root logger once at startup. |
| 56 | +# LOG_LEVEL.upper() converts e.g. "info" → "INFO" to match the constants |
| 57 | +# expected by the logging module (logging.INFO, logging.DEBUG, etc.). |
| 58 | +# |
| 59 | +# Use `logger.info(...)`, `logger.warning(...)`, `logger.error(...)` etc. |
| 60 | +# throughout your code instead of print(). Benefits: |
| 61 | +# • Timestamps, severity levels, and module names are added automatically. |
| 62 | +# • Output can be redirected to files or log aggregators without code changes. |
| 63 | +# • Verbosity is controlled at runtime via LOG_LEVEL, not by editing code. |
18 | 64 | logging.basicConfig(level=LOG_LEVEL.upper()) |
19 | 65 | logger = logging.getLogger(__name__) |
| 66 | +# __name__ is the module's fully-qualified name (e.g. "main"). |
| 67 | +# Using __name__ means log messages identify which module produced them. |
20 | 68 |
|
21 | 69 |
|
22 | | -# ── App ──────────────────────────────────────────────────────────────────── |
23 | | - |
| 70 | +# ── Application factory ──────────────────────────────────────────────────── |
| 71 | +# FastAPI() creates the ASGI application object. Everything—routes, |
| 72 | +# middleware, startup hooks—is registered on this object. |
| 73 | +# |
| 74 | +# The metadata arguments (title, description, version) appear in: |
| 75 | +# • The auto-generated /docs (Swagger UI) page. |
| 76 | +# • The /openapi.json schema that clients can use to generate SDK code. |
24 | 77 | app = FastAPI( |
25 | 78 | title="Ground Control Station", |
26 | 79 | description="CMP9134 — Robot Management System scaffold", |
27 | 80 | version="0.1.0", |
28 | 81 | ) |
29 | 82 |
|
| 83 | +# ── CORS middleware ──────────────────────────────────────────────────────── |
| 84 | +# Browsers enforce the Same-Origin Policy: a web page served from one origin |
| 85 | +# (e.g. http://localhost:3000) is NOT allowed to make fetch() requests to a |
| 86 | +# different origin (e.g. http://localhost:8000) unless that second origin |
| 87 | +# explicitly opts in via CORS headers. |
| 88 | +# |
| 89 | +# CORSMiddleware adds the necessary HTTP response headers so the browser |
| 90 | +# permits the cross-origin request. |
| 91 | +# |
| 92 | +# allow_origins=["*"] |
| 93 | +# Permit requests from ANY origin. Fine for development or a controlled |
| 94 | +# lab network, but in production you should list specific allowed origins |
| 95 | +# (e.g. ["https://yourapp.example.com"]) to prevent abuse. |
| 96 | +# |
| 97 | +# allow_credentials=True |
| 98 | +# Allow cookies and HTTP authentication headers to be sent cross-origin. |
| 99 | +# Note: this cannot be combined with allow_origins=["*"] in a real |
| 100 | +# production deployment — the browser will reject it. |
| 101 | +# |
| 102 | +# allow_methods=["*"], allow_headers=["*"] |
| 103 | +# Don't restrict which HTTP verbs or request headers are allowed. |
30 | 104 | app.add_middleware( |
31 | 105 | CORSMiddleware, |
32 | 106 | allow_origins=["*"], |
|
37 | 111 |
|
38 | 112 |
|
39 | 113 | # ── Health check ─────────────────────────────────────────────────────────── |
40 | | - |
41 | | - |
| 114 | +# A health check endpoint is a simple route that returns 200 OK when the |
| 115 | +# server is running correctly. It is called by: |
| 116 | +# • Docker — via the HEALTHCHECK instruction in the Dockerfile, which marks |
| 117 | +# the container "healthy" once this endpoint responds successfully. |
| 118 | +# • The CI pipeline — the compose-test job polls this URL before running |
| 119 | +# further tests (see .github/workflows/ci.yml). |
| 120 | +# • Load balancers — to decide whether to route traffic to this instance. |
| 121 | +# |
| 122 | +# include_in_schema=False hides this internal route from the /docs page |
| 123 | +# because it is infrastructure plumbing, not part of the public API. |
| 124 | +# |
| 125 | +# Note: this is a plain `def` (synchronous), not `async def`. That is fine |
| 126 | +# here because the function does no I/O — it just returns a dict immediately. |
| 127 | +# FastAPI handles both sync and async handlers correctly. |
42 | 128 | @app.get("/health", include_in_schema=False) |
43 | 129 | def health(): |
44 | 130 | return {"status": "ok"} |
45 | 131 |
|
46 | 132 |
|
47 | | -# ── Example: robot status ────────────────────────────────────────────────── |
| 133 | +# ── Robot status proxy ───────────────────────────────────────────────────── |
48 | 134 | # TODO: add authentication, RBAC, and mission logging around these endpoints. |
49 | | - |
50 | | - |
| 135 | +# |
| 136 | +# This route is `async def` because it calls `await robot.get_status()`, |
| 137 | +# which performs a network request to the robot simulator. |
| 138 | +# |
| 139 | +# Why async matters for network I/O |
| 140 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 141 | +# FastAPI uses an async event loop (from the `asyncio` standard library). |
| 142 | +# When an async handler reaches an `await`, it *suspends itself* and lets the |
| 143 | +# event loop handle other incoming requests, rather than blocking the whole |
| 144 | +# process. This means one Python process can serve many concurrent requests |
| 145 | +# without needing a thread per request — crucial for a real-time robotics |
| 146 | +# dashboard where many clients may be connected simultaneously. |
| 147 | +# |
| 148 | +# Error handling pattern |
| 149 | +# ~~~~~~~~~~~~~~~~~~~~~~ |
| 150 | +# We catch `RobotConnectionError` specifically — not bare `Exception`. |
| 151 | +# Catching only the errors we expect means unexpected bugs (e.g. a TypeError |
| 152 | +# from bad data) still propagate and cause a proper 500 response, rather than |
| 153 | +# being silently swallowed by an overly broad except clause. |
| 154 | +# |
| 155 | +# Returning {"error": str(exc)} gives the frontend a machine-readable message |
| 156 | +# instead of letting FastAPI produce an unhelpful 500 HTML page. |
51 | 157 | @app.get("/api/status") |
52 | 158 | async def get_status(): |
53 | | - """Proxy the robot status. Replace/extend with your own logic.""" |
| 159 | + """Return the current robot status (position, battery level, state). |
| 160 | +
|
| 161 | + Proxies the request to the Virtual Robot API via ``robot_client.RobotClient``. |
| 162 | + Returns the robot's JSON payload directly, or an error dict if the robot |
| 163 | + simulator is unreachable. |
| 164 | + """ |
54 | 165 | try: |
55 | 166 | return await robot.get_status() |
56 | 167 | except RobotConnectionError as exc: |
| 168 | + # Log the error server-side at WARNING level so it appears in the |
| 169 | + # server logs without spamming at ERROR level for expected downtime. |
| 170 | + logger.warning("Could not reach robot API: %s", exc) |
57 | 171 | return {"error": str(exc)} |
58 | 172 |
|
59 | 173 |
|
60 | 174 | # ── TODO: add your routes below ──────────────────────────────────────────── |
61 | | -# Example route skeletons: |
| 175 | +# Use the skeletons below as starting points. Each route should: |
| 176 | +# 1. Validate inputs — FastAPI does this automatically when you add type |
| 177 | +# hints to the function parameters (e.g. `x: int`). |
| 178 | +# 2. Call the appropriate RobotClient method from robot_client.py. |
| 179 | +# 3. Handle RobotConnectionError (and any other expected errors) gracefully. |
| 180 | +# 4. Return a meaningful JSON response. |
62 | 181 | # |
63 | 182 | # @app.post("/api/move") |
64 | 183 | # async def move(x: int, y: int): |
65 | | -# ... |
| 184 | +# """Send the robot to position (x, y).""" |
| 185 | +# try: |
| 186 | +# return await robot.move(x, y) |
| 187 | +# except RobotConnectionError as exc: |
| 188 | +# logger.warning("Move command failed: %s", exc) |
| 189 | +# return {"error": str(exc)} |
66 | 190 | # |
67 | 191 | # @app.websocket("/ws/telemetry") |
68 | 192 | # async def ws_telemetry(websocket: WebSocket): |
69 | | -# ... |
| 193 | +# """Stream live sensor data to a connected browser client. |
| 194 | +# |
| 195 | +# WebSockets maintain a persistent two-way connection, making them ideal |
| 196 | +# for low-latency telemetry feeds (position, battery, sensor readings). |
| 197 | +# Unlike HTTP, you don't need to poll — the server pushes updates. |
| 198 | +# """ |
| 199 | +# await websocket.accept() |
| 200 | +# try: |
| 201 | +# while True: |
| 202 | +# data = await robot.get_status() |
| 203 | +# await websocket.send_json(data) |
| 204 | +# await asyncio.sleep(0.5) # push an update every 500 ms |
| 205 | +# except WebSocketDisconnect: |
| 206 | +# logger.info("Telemetry client disconnected") |
0 commit comments