src/service/) is the business-logic tier of the gateway. It sits between the HTTP/Socket.IO routes and the SQLAlchemy models plus every external system the gateway talks to: drones over rosbridge, Janus video rooms, AWS (S3/EC2/ECR), Docker, Stripe, and SMTP. Routes stay thin — parse the request, check auth, delegate to a service, serialize the result.
There is no Flask dependency-injection container here. Every service is instantiated once at import time as a module-global in the composition root src/application/app.py, and route blueprints import those singletons by name. This page maps that wiring, the two lazy-initialization escape hatches (get_service and get_vpn_service), and the DroneService → Physical/SITL inheritance the factory returns.
Deep dives on each service live on sibling pages — see DroneControlService & rosbridge dispatch, SITL lifecycle, video rooms, assets & archives, executions & reports, billing, and platform services. This page is only about how they are constructed and injected.
Composition root: app.py
Importing src.application.app builds the entire singleton graph as a side effect. Order matters because later services take earlier ones as constructor arguments (drone_control_service and asset_service feed into physical_drone_service, for example).
src/application/app.py
| Singleton | Class | Constructed with | Notes |
|---|---|---|---|
video_service | VideoService | — | Janus VideoRoom management |
email_service | EmailService | — | SMTP wrapper, shared by user + report services |
user_service | UserService | db, Bcrypt(...), email_service | Also aliased as generic_user_service |
generic_drone_service | DroneService | db | Base CRUD; used directly by routes for lookups |
generic_mission_service | MissionService | db | Aliased in routes as mission_service |
point_service | PointService | db | |
geofence_service | GeofenceService | db | |
drone_control_service | DroneControlService | generic_drone_service, generic_mission_service | Built without SocketIO (attached later — see below) |
asset_service | AssetService | db | |
physical_drone_service | PhysicalDroneService | db, video_service, drone_control_service, asset_service | Eager; the physical entry in the factory registry |
subscription_service | SubscriptionService | db | Stripe billing |
calendar_service | CalendarService | db | |
execution_service | ExecutionService | db | |
report_service | ReportService | db, email_service | |
archive_service | ArchiveService | db | |
log_analysis_service | LogAnalysisService | — | pymavlink .bin parsing |
Not every service is constructed here.
IsaacSimService is instantiated directly in src/routes/isaac_sim_routes.py:16, not in app.py. IPService is used statically (no instance). VPN_Service is built lazily via get_vpn_service(). And AWSInstanceService / InstanceSchedulerService are dormant — defined but never instantiated anywhere in the current codebase.Dependency injection into routes
“DI” here is just Python module imports. Blueprints pull the singletons they need straight offapp.py:
src/routes/drone_routes.py
user_routes imports user_service, billing_routes imports subscription_service, execution_routes imports the execution/report/asset trio, and so on). To swap an implementation for a test, you patch the module global — there is no registry to override.
Startup wiring order (load-bearing)
DroneControlService is constructed in app.py without a SocketIO instance, because the gevent SocketIO object does not exist yet. src/main.py creates SocketIO, wires the socket routes, and only then attaches it back onto the already-built service:
src/main.py
DroneControlService only registers the telemetry-emit callback on a connection when self.socketio is truthy, so telemetry cannot flow until line 259 runs. See Connection Pool & Startup Wiring for the full sequence.
The get_service(drone_type) factory
Drone lifecycle operations (create, update, delete) are polymorphic across drone types, so routes never reference a concrete drone service directly. Instead they call get_service(drone_type) and get back the right DroneService subclass.
src/application/app.py
DroneTypes enum values "physical" and "sitl" (src/utils/drone_types.py). Anything else raises UnknownDroneTypeException (src/utils/unknown_drone_type.py).
Physical is eager
physical_drone_service is created at import time and pre-seeded into the drone_services registry, so get_service("physical") is a plain dict lookup.SITL is lazy behind ENABLE_SITL
SITLDroneService is only constructed on the first get_service("sitl") call, and only when settings.ENABLE_SITL is true (default true, from ENABLE_SITL env var). Its constructor connects to Docker (local docker.from_env() or a remote host over the jumphost tunnel), so this lazy pattern avoids paying that cost when SITL is disabled. Once built, it is cached in drone_services for the rest of the process.get_vpn_service() lazy init
VPN_Service is the one supporting service deliberately kept out of the eager block. It builds a boto3 S3 client in its constructor, and that client binds to settings.REGION — which must be loaded and validated first. The lazy singleton guarantees the region is set before the client is created:
src/application/app.py
get_vpn_service() at request time (drone_routes.py, vpn_routes.py) rather than importing an instance. It presigns per-user / per-drone WireGuard configs and proxies VPN status checks — details on VPN IP Authentication & Jumphost Routing.
DroneService → Physical / SITL inheritance
The factory always returns a DroneService subclass. The base class owns generic CRUD and the rich lookup surface; the two subclasses override only the lifecycle methods that differ.
| Concern | DroneService (base) | PhysicalDroneService | SITLDroneService |
|---|---|---|---|
save() | Sync; 3× retry in a nested tx; generates a 10-digit activation_token only when type == "physical" | async; allocates a real IP via IPService.get_drone_ip() (ip == mac), calls super().save, then creates a Janus room + JWT room token | async; enforces USER_SITL_MAX_COUNT, spins up the 3-container Docker stack, saves with ip == mac == container_name and port = 9090 + n |
delete() | Sync DB delete by (id, user_id) | async; deletes S3 assets → Janus room → DB row | async; deletes S3 assets → stops core/gamepad/SITL containers (reverse order) → DB row |
activate() | — | Clears activation_token | (inherits base) |
Lookups (get_drone_by_id, _by_ip, _by_token, counts) | ✅ owns all | inherited | inherited |
| Constructor args | db | db, video_service, drone_control_service, asset_service | same four args |
save()/delete() are async on the subclasses but sync on the base, routes await the factory result (await drone_service.save(data) in drone_routes.py:111). The route also branches on return shape: SITL save() returns a dict wrapping the drone, physical save() returns the drone dict directly.
Adding a new drone type or service
Add the enum value
Extend
DroneTypes in src/utils/drone_types.py. The factory keys strictly off these enum values, so an unlisted string can never resolve.Implement the subclass
Subclass
DroneService and override save/delete (mark them async to match how routes await them). Reuse the base lookups and generate_activation_token().Register it in the factory
Either eager-seed it into the
drone_services dict next to physical (like PhysicalDroneService), or add a lazy branch in get_service behind its own feature flag (like SITL). Prefer lazy if the constructor touches an external system (Docker, boto3, a socket).Wire supporting singletons in app.py
If it is a plain supporting service (not a drone type), instantiate it in the
app.py composition block in dependency order, then import the singleton in the consuming blueprint. If its constructor needs a late-bound resource (region, SocketIO), follow the get_vpn_service lazy pattern or the main.py post-init attachment pattern instead.ENABLE_SITL, DEPLOYMENT_ENVIRONMENT, REMOTE_DOCKER_ENABLED, REGION) and how startup validation fails fast, see Gateway Environment Variables and Startup, Validation & Composition Root.
