UserService), VPN address allocation (IPService), the mission / waypoint / geofence CRUD trio, recurring calendar scheduling (CalendarService), and the Isaac Sim EC2 fleet (IsaacSimService plus two dormant siblings). These are ordinary SQLAlchemy-backed services with a few sharp edges — a hex-encoded password round-trip and a mostly-inactive Isaac Sim scheduler chief among them.
All of these services are constructed as module-global singletons in src/application/app.py and injected into route blueprints. For the wiring model and the get_service() drone factory, see Service Layer & get_service Factory.
| Service | File | Constructed in app.py | Consumed by |
|---|---|---|---|
UserService | src/service/user_service.py | user_service = UserService(db, Bcrypt(...), email_service) | auth_routes, user_routes |
IPService | src/service/ip_service.py | static methods (never instantiated) | UserService, PhysicalDroneService |
MissionService | src/service/mission_service.py | generic_mission_service = MissionService(db) | mission_routes, DroneControlService |
PointService | src/service/point_service.py | point_service = PointService(db) | mission_routes |
GeofenceService | src/service/geofence_service.py | geofence_service = GeofenceService(db) | geofence_routes, DroneControlService |
CalendarService | src/service/calendar_service.py | calendar_service = CalendarService(db) | calendar_routes |
IsaacSimService | src/service/isaac_sim_service.py | instantiated in isaac_sim_routes.py, not app.py | isaac_sim_routes |
UserService — bcrypt auth, email flows & the hex round-trip
UserService (src/service/user_service.py) owns user creation, authentication material, and the email-driven verification / password-reset flows. It depends on flask_bcrypt.Bcrypt, itsdangerous.URLSafeTimedSerializer, and EmailService.
The password round-trip you must preserve
This is the single most important behavior on the page. Passwords are bcrypt-hashed and then stored hex-encoded in theuser.password column. Every getter reverses the encoding before returning the ORM object:
src/service/user_service.py
get_user_by_name, get_user_by_id, and get_user_by_email all mutate the in-memory User.password into raw bcrypt bytes — that is what check_password_hash expects. The login route relies on exactly this round-trip:
src/routes/auth_routes.py
create() and save(user, new_password=True) are the write side — they run bcrypt.generate_password_hash(user.password) before committing. save() only re-hashes when new_password=True and a password is present, so it doubles as a plain profile-save when called with defaults.
Registration, verification & reset
Request verification email
send_verify_email(email, origin) signs {"email", "origin"} with URLSafeTimedSerializer(JWT_SECRET_KEY) under salt=secret_key and emails a link of the form {origin}/reset-password?token=<token>&verify=true.Create the user (token redeemed)
create_user(token, password) calls serializer.loads(token, salt=secret_key, max_age=3600) — a 1-hour expiry. It rejects an already-existing email, then allocates a VPN address via IPService.get_user_ip() and inserts the User inside a 3-attempt session.begin(nested=True) loop that retries on IntegrityError (concurrent IP / email collisions).Two different token mechanisms coexist here: itsdangerous
URLSafeTimedSerializer (email verification, 1h, salted with the JWT secret) and Flask-JWT create_access_token (password-reset links). The auth model and JWT lifecycle are documented in Authentication & JWT Lifecycle.get_seed_user() returns the first user whose email matches e2e-% — a hook for the end-to-end test suite. Deletes (delete_user_by_id / delete_user) are hard row deletes; account teardown of the user’s drones, missions, geofences, and calendar events is handled by the per-user cleanup helpers on the other services below.
IPService — CIDR allocation for the WireGuard planes
IPService (src/service/ip_service.py) is a static-only class that hands out the next free address from two WireGuard CIDRs. Users live on the user plane, drones on the drone plane; both default to /16 networks and are configurable via environment variables.
| Method | Pool (env var) | Default CIDR | Used by |
|---|---|---|---|
get_user_ip() | USER_NETWORK_CIDR | 10.70.0.0/16 | UserService.create_user |
get_drone_ip() | DRONE_NETWORK_CIDR | 10.71.0.0/16 | PhysicalDroneService.save |
is_ip_in_user_cidr(ip) | USER_NETWORK_CIDR | 10.70.0.0/16 | (no callers — defined but unused) |
is_ip_in_drone_cidr(ip) | DRONE_NETWORK_CIDR | 10.71.0.0/16 | (no callers — defined but unused) |
ipaddress.ip_network(cidr).hosts() in order and returns the first host address not already taken and not in the reserved set. The network object is memoized with @lru_cache(maxsize=3). The reserved set excludes the gateway address (.0.1) and a .0.255 sentinel in addition to the network/broadcast addresses that hosts() already skips — so the first user address handed out on a fresh 10.70.0.0/16 is 10.70.0.2.
src/service/ip_service.py
None when the pool is exhausted; callers are expected to surface that as an error. The broader plane topology (jumphost, per-user isolation, drone-plane iptables) is covered in Network & VPN Topology and VPN IP Authentication & Jumphost Routing.
Missions, Points & Geofences
Three thin CRUD services back the map-editing surface. They are pure data services — the actual upload to a vehicle is done byDroneControlService (see DroneControlService & Rosbridge Dispatch), and the on-wire MAVLink encoding is documented in Mission & Geofence MAVLink Format.
MissionService—Mission+MissionPointCRUD withjoinedload(Mission.mission_points).create_mission_pointdefaults the MAVLink fields (frame=3,command=16,autocontinue=True,param1..4=0.0) so the frontend can send bare lat/lng/altitude/sequence.delete(mission_id, user_id)first NULLsDrone.mission_idfor any drone pointing at the mission, then deletes its points, then the mission row — order matters to avoid FK violations.PointService— a thinnerMissionPoint-only service used by mission routes:save,delete,update(rebuilds aMissionPointthenfilter(...).update(to_dict())), andbatch_update(loops per point in one commit).GeofenceService—Geofence+GeofencePointCRUD, plustoggle_geofence(flipsenabled),update_geofence/update_geofence_point(field-by-field),batch_update_points, and per-user cleanup (delete_geofences_by_user_id,delete_geofence_points_by_user_id). Points cascade-delete with their geofence. Geofence points defaultframe=3but leavecommandunset —DroneControlService.push_geofencederives the MAVLink command from the fence type at push time.
GeofenceService is imported lazily inside DroneControlService.push_geofence / sync_geofences (along with get_fence_command) to avoid an import cycle. Preserve those in-function imports if you refactor the service layer. The HTTP surface for both lives in Missions & Geofences API.user_id in the WHERE clause, so a mission or geofence can only be read or mutated by its owner — ownership is enforced per-query here, unlike the drone-command telemetry rooms which enforce ownership only at connection creation.
CalendarService — recurring events with occurrence exceptions
CalendarService (src/service/calendar_service.py) schedules flights against a drone + mission and supports iCal recurrence rules. Its data model is two tables:
CalendarEvent—title,description,scheduled_time(timezone-aware),status(EventStatus=scheduled/completed/cancelled),drone_id,mission_id, optionalexecution_id, andrecurrence_rule(an iCalRRULEstring).is_recurring()is simplybool(recurrence_rule).CalendarEventOccurrence— a sparse exception table keyed by(event_id, occurrence_time). It only stores deviations from the default (a changedstatusand/or an attachedexecution_id); unmodified occurrences are never persisted.
Recurrence is expanded at read time
There is no materialized occurrence per date.get_events(user_id, start, end) fetches candidate events (non-recurring in-range, plus any recurring event whose scheduled_time <= end) and, for recurring events, expands them on the fly with dateutil.rrule.rrulestr(...).between(start, end, inc=True), overlaying any stored exceptions:
src/service/calendar_service.py
is_occurrence=True, its own scheduled_time, and the original stored under original_scheduled_time.
Occurrence status updates are upserts
update_occurrence_status(...) and delete_occurrence(...) first validate that the target time is a real occurrence of the rule (_is_valid_occurrence checks rule.between(t, t, inc=True)). Then:
- Setting an occurrence back to
scheduleddeletes its exception row (returning it to the default) rather than storingscheduled. - Any other status (
completed,cancelled) upserts an exception row;delete_occurrenceis implemented as an upsert tocancelled.
recurrence_rule triggers _cleanup_invalid_exceptions: if recurrence is removed entirely, all exception rows are deleted; otherwise any exception whose time is no longer a valid occurrence of the new rule is pruned. Validation guards throughout raise ValueError (missing/invalid datetime, unknown drone/mission/execution, editing a completed non-recurring event), which the routes translate to 400s.
The HTTP endpoints (/calendar/events and /calendar/events/{id}/occurrences/..., all JWT-required) are documented in Billing, Calendar, VPN, Video & Isaac Sim API.
Isaac Sim EC2 services
Three services target NVIDIA Isaac Sim GPU instances on EC2. Only one is live; the other two are dormant scaffolding.IsaacSimService (active)
IsaacSimService (src/service/isaac_sim_service.py) manages one hardcoded instance and is instantiated directly in src/routes/isaac_sim_routes.py (not in app.py). It exposes four JWT-protected endpoints under /api/v1/isaac-sim:
| Endpoint | Method call | Purpose |
|---|---|---|
GET /isaac-sim/status | get_instance_status() | State, public/private IP, runtime, and a live cost estimate |
POST /isaac-sim/start | start_instance() | Start when stopped (guards other states) |
POST /isaac-sim/stop | stop_instance() | Stop when running, reporting session cost |
GET /isaac-sim/metadata | get_instance_metadata() | Static specs (GPU, ports, software versions) |
src/service/isaac_sim_service.py
get_instance_status() computes runtime from LaunchTime and estimates cost at a $0.95/hr on-demand rate plus prorated 150 GB gp3 storage. get_instance_metadata() reports the fixed spec: NVIDIA L40S (48 GB), Isaac Sim 4.2.0, streaming port 8211, WebRTC ports 47995-47999. It uses a plain boto3.client("ec2", region_name="eu-central-1") — the caller’s AWS credentials/role must permit ec2:DescribeInstances / StartInstances / StopInstances on that instance.
Isaac Sim itself (Visual SLAM, GPS-denied navigation) runs on the drone, not the Gateway — see Isaac Visual SLAM & Pose Bridge. This service only powers the cloud instance the Dashboard’s Isaac Sim panel starts and stops.
AWSInstanceService & InstanceSchedulerService (dormant)
Gotchas to preserve
Password column is hex-encoded bcrypt, not raw bcrypt
Password column is hex-encoded bcrypt, not raw bcrypt
The DB stores
\x-prefixed hex; get_user_by_* decode it in place via binascii.unhexlify(pw.encode().lstrip(b"\\x")). Authentication (check_password_hash) depends on this exact decode. Never bypass the getters for an auth read, and never re-persist a decoded User.SITL drones break naive Drone.ip handling in IPService
SITL drones break naive Drone.ip handling in IPService
get_drone_ip() uses a CASE to swap in SITL_HOST for the INET sort cast because SITL rows store a container name in Drone.ip. The membership set keeps raw values, so container names never collide with drone-plane addresses.Calendar occurrences are computed, exceptions are sparse
Calendar occurrences are computed, exceptions are sparse
Recurring events are expanded at read time via
dateutil.rrule; only status/execution deviations are stored in CalendarEventOccurrence. Setting an occurrence back to scheduled deletes its row. Changing a rule prunes now-invalid exceptions.Two of the three Isaac Sim services are dead code
Two of the three Isaac Sim services are dead code
Only
IsaacSimService (single hardcoded instance) is live. AWSInstanceService / InstanceSchedulerService are uninstantiated, and the scheduler’s import path is broken under the src. package layout.Related pages
Service Layer & Factory
How every service is wired as a singleton and the
get_service() drone factory.Authentication & JWT Lifecycle
Login, refresh, verification and reset token mechanics that sit on top of
UserService.Missions & Geofences API
The HTTP surface backed by Mission/Point/Geofence services.
Isaac Sim Tables (Raw SQL)
The non-ORM tables the dormant Isaac Sim scheduler targets.

