Skip to content

Software Architecture

Junior Boni edited this page Oct 4, 2026 · 25 revisions

5.4 Software Architecture / System Overview

High-Level Diagram

image

Components

  • Robot Software: runs onboard (ROS 2 nodes, SLAM, Nav2)
  • Ingestion Service: receives session data/video/map uploads from robot
  • Object Storage: raw 3D maps, video, images
  • Processing Service: turns raw scans into usable 3D tiles/reports
  • Web / Mobile Application: client-facing teleoperation + dashboard

Key Frameworks & Libraries

See External Libraries for the full list (ROS 2, Nav2, SLAM framework, COLMAP, OpenMVS, Three.js, Cesium 3D Tiles, React, React Native, PostgreSQL, Node.js).

Use Cases

The most up to date version of the use case diagrams can be viewed here. Please contact @RealBJr for editing access to this artifact.

General User Operations

image

On-Site Admin Operations

image

System Admin Operations

image

Critical Use Cases

Limited to use cases the actors (User / On-Site Admin) directly interact with in the RoamView application layer; backend service internals (Processing Service, Ingestion Service) excluded. See this diagram for more details. Specific requirement details are available in the Software Specification section.

Diagrams: GU = General User Operations  |  OA = On-Site Admin Operations  |  SA = System Admin
Actors: User, On-Site Admin, Admin(System Admin)

ID Diagram Use Case Actor(s) Relationship Related Use Case(s) Related Card(s)
UC-GU01 GU Live Walkthrough User, On-Site Admin, System Admins Include UC-GU08 —
UC-GU02 GU Auth User, On-Site Admin, System Admins — — —
UC-GU03 GU Load publicly available Meshes User, On-Site Admin, System Admins — — —
UC-GU04 GU Load authorized private Meshes User, On-Site Admin, System Admins Extend UC-GU06 —
UC-GU05 GU Load mesh scans history User, On-Site Admin, System Admins Extend UC-GU07 —
UC-GU06 GU Compare mesh version User, On-Site Admin, System Admins — — —
UC-GU07 GU Send Video Stream Robot — — —
UC-GU08 GU Send Video Recording Robot — — —
UC-GU09 GU Compare mesh version Robot — — —
UC-GU10 GU Send 3D mesh Robot — — —
UC-GU11 GU Annotate Generated Mesh User, On-Site Admin, System Admins Extend UC-GU03, UC-GU04 —
UC-OA01 OA Schedule Inspections On-Site Admin Include UC-OA02 —
UC-OA02 OA Auth On-Site Admin — — —
UC-OA03 OA Load Generated Meshes On-Site Admin Include UC-OA04 —
UC-SA01 SA Load Stats and Historical Data System Admins — — —
UC-SA02 SA Privatize Meshes System Admins — — —
UC-SA03 SA Load Stats and Historical Data Manage Accounts — — —

Implementation Details

UC-GU01: Live Walkthrough

image

UC-GU03: Load publicly available meshes

image

UC-GU04: Load authorized private Meshes

image

UC-GU08: Send Video Recording

image

UC-OA01: Schedule Inspections

image

UC-OA03: Load Generated Meshes

image

Class Diagrams

Backend / web domain model

classDiagram
    direction TB
    class Organization {
        +UUID id
        +String name
    }
    class User {
        +UUID id
        +String email
        +Role role
        +authenticate() bool
    }
    class Role {
        <<enumeration>>
        USER
        ON_SITE_ADMIN
    }
    class Site {
        +UUID id
        +String name
        +String address
    }
    class Robot {
        +UUID id
        +String serialNumber
        +RobotStatus status
        +isOnline() bool
    }
    class Session {
        +UUID id
        +SessionMode mode
        +SessionStatus status
        +String videoStorageKey
        +start() void
        +end() void
    }
    class SessionMode {
        <<enumeration>>
        AUTONOMOUS
        LIVE_WALKTHROUGH
    }
    class Mesh {
        +UUID id
        +MeshVisibility visibility
        +String objectStorageKey
        +String tilesetKey
        +int version
        +getSignedUrl() String
    }
    class Annotation {
        +UUID id
        +Geometry position
        +String label
        +Severity severity
    }
    class Measurement {
        +UUID id
        +float valueMeters
    }
    class InspectionReport {
        +UUID id
        +String pdfStorageKey
        +generate() void
    }
    class ScheduledInspection {
        +UUID id
        +DateTime runAt
        +createJob() InspectionJob
    }
    class InspectionJob {
        +UUID id
        +JobStatus status
        +store() void
        +notifyRobot() void
    }
    Organization "1" --> "*" User : employs
    Organization "1" --> "*" Site : owns
    Organization "1" --> "*" Robot : owns
    User "1" --> "0..*" Session : operates
    Site "1" --> "*" Session : hosts
    Site "1" --> "0..1" Robot : assigned
    Robot "1" --> "*" Session : performs
    Session "1" --> "0..1" Mesh : produces
    Mesh "0..1" --> "0..1" Mesh : previousVersion
    Mesh "1" --> "*" Annotation : has
    Mesh "1" --> "*" Measurement : has
    User "1" --> "*" Annotation : authors
    Session "1" --> "0..1" InspectionReport : generates
    Site "1" --> "*" ScheduledInspection : schedules
    Robot "1" --> "*" ScheduledInspection : target
    ScheduledInspection "1" --> "0..1" InspectionJob : creates
    InspectionJob "1" --> "0..1" Session : resultsIn
    User ..> Role : role
    Session ..> SessionMode : mode
Loading

Domain Model

classDiagram
    direction TB

    class Organization {
        name
    }
    class User {
        email
        role
    }
    class Site {
        name
        address
    }
    class Robot {
        serialNumber
        status
    }
    class Session {
        mode
        status
        startedAt
        endedAt
    }
    class Mesh {
        <<3D Map>>
        visibility
        version
        objectStorageKey
    }
    class Annotation {
        label
        severity
        position
    }
    class Measurement {
        type
        valueMeters
    }
    class InspectionReport {
        title
        generatedAt
    }
    class ScheduledInspection {
        runAt
        status
    }
    class InspectionJob {
        status
    }

    Organization "1" --> "*" User : employs
    Organization "1" --> "*" Site : owns
    Organization "1" --> "*" Robot : owns
    User "1" --> "0..*" Session : operates
    Site "1" --> "*" Session : hosts
    Site "1" --> "0..1" Robot : assigned
    Robot "1" --> "*" Session : performs
    Session "1" --> "0..1" Mesh : produces
    Mesh "0..1" --> "0..1" Mesh : previousVersion
    Mesh "1" --> "*" Annotation : has
    Mesh "1" --> "*" Measurement : has
    User "1" --> "*" Annotation : authors
    User "1" --> "*" Measurement : authors
    Session "1" --> "0..1" InspectionReport : generates
    Site "1" --> "*" ScheduledInspection : schedules
    Robot "1" --> "*" ScheduledInspection : target
    ScheduledInspection "1" --> "0..1" InspectionJob : creates
    InspectionJob "1" --> "0..1" Session : resultsIn
Loading

Core entities

Organization — Tenant that owns users, sites, and robots. User — Signs in to RoamView. Role is User or On-Site Admin. Site — Physical location. At most one robot is assigned to a site. Robot — On-site unit. Runs an autonomous inspection or a live walkthrough. Session — One walkthrough. Mode is autonomous or live walkthrough. Status moves from recording through processing to ready. Mesh — 3D map from a session. Public or private. previousVersion links maps for compare-mesh-version. File bytes are in object storage; this entity holds the key. Annotation / Measurement — Pins and distances on a generated mesh. Inspection report — Summary of a session. PDF is in object storage. Scheduled inspection / Inspection job — On-site admin books a run; that creates a job; the robot is notified; a completed job becomes a session.

ER-Diagram

erDiagram
    direction TB
    organizations {
        uuid id PK
        string name
        string slug
        timestamp created_at
    }
    users {
        uuid id PK
        uuid org_id FK
        string email
        string password_hash
        string role
        timestamp created_at
    }
    sites {
        uuid id PK
        uuid org_id FK
        string name
        string address
        geometry footprint
        timestamp created_at
    }
    robots {
        uuid id PK
        uuid org_id FK
        uuid site_id FK
        string name
        string serial_number
        string status
        int battery_pct
        timestamp last_seen_at
    }
    sessions {
        uuid id PK
        uuid site_id FK
        uuid robot_id FK
        uuid operator_id FK
        string mode
        string status
        string video_storage_key
        timestamp started_at
        timestamp ended_at
    }
    meshes {
        uuid id PK
        uuid session_id FK
        uuid previous_mesh_id FK
        string visibility
        int version
        string object_storage_key
        string tileset_key
        string preview_key
        timestamp created_at
    }
    annotations {
        uuid id PK
        uuid mesh_id FK
        uuid author_id FK
        geometry position
        string label
        string severity
        text notes
        timestamp created_at
    }
    measurements {
        uuid id PK
        uuid mesh_id FK
        uuid author_id FK
        string type
        jsonb points
        float value_meters
        timestamp created_at
    }
    reports {
        uuid id PK
        uuid session_id FK
        uuid author_id FK
        string title
        string pdf_storage_key
        timestamp generated_at
    }
    scheduled_inspections {
        uuid id PK
        uuid site_id FK
        uuid robot_id FK
        timestamp run_at
        string status
    }
    inspection_jobs {
        uuid id PK
        uuid schedule_id FK
        uuid session_id FK
        string status
    }
    organizations ||--o{ users : has
    organizations ||--o{ sites : owns
    organizations ||--o{ robots : owns
    sites ||--o| robots : assigned
    users ||--o{ sessions : operates
    sites ||--o{ sessions : hosts
    robots ||--o{ sessions : performs
    sessions ||--o| meshes : produces
    sessions ||--o| reports : generates
    meshes |o--o| meshes : previous_version
    meshes ||--o{ annotations : has
    meshes ||--o{ measurements : has
    users ||--o{ annotations : authors
    users ||--o{ measurements : authors
    sites ||--o{ scheduled_inspections : schedules
    robots ||--o{ scheduled_inspections : target
    scheduled_inspections ||--o| inspection_jobs : creates
    inspection_jobs ||--o| sessions : results_in
Loading

3D map storage

3D maps are not stored as PostgreSQL BLOBs. meshes.object_storage_key and meshes.tileset_key point at object storage. The same pattern applies to sessions.video_storage_key and reports.pdf_storage_key. The API returns a signed URL; the client streams bytes from object storage / CDN.

Notes

  • PostgreSQL stores structured metadata. Large binaries (3D maps, video, tiles) are stored in object storage and referenced by key.

Notation

Crow’s-foot marks show how many rows participate in a relationship.

Symbol Meaning
|| Exactly one (required)
○| Zero or one (optional)
○< Zero or more (optional many)

PK is the primary key (unique id of a row). FK is a foreign key (points at another table’s PK).

Data Model & Storage

RoamView keeps data in two storage systems:

System Technology What it holds Access pattern
General storage (metadata) PostgreSQL Structured, queryable rows: organizations, users, sites, robots, sessions, jobs, meshes, annotations, reports Small rows, transactional, joined and filtered by the API
Object storage (BLOBs) S3-compatible object store (e.g. AWS S3 / MinIO) Large binaries: raw photo sets, depth frames, telemetry bags, video, meshes, 3D tilesets, report PDFs Written once, read many times, streamed to clients through signed URLs / CDN

Rule: PostgreSQL never stores file bytes. Tables store the object key (the file's path in object storage), as in the ER diagram on Software Architecture. This replaces the "BLOB storage for 3D maps" note in External Libraries.

Naming used in this page vs. the task card

The task card uses early names. This page uses the names from Software Architecture and the Domain Model.

Task card term Name in this model
Facility sites
User users
ScanJob inspection_jobs
Tile meshes.tileset_key (the tileset's root file). Each tile file exists only in object storage (see 3D tiles)

1. General Storage (Metadata) — ER Diagram

The tables from the ER diagram on Software Architecture are unchanged. One table is added, storage_objects, which lists every file in object storage. It does not replace any existing column. It sits alongside them and is matched to them by key (see section 3).

erDiagram
    direction TB
    organizations {
        uuid id PK
        string name
        string slug
        timestamp created_at
    }
    users {
        uuid id PK
        uuid org_id FK
        string email
        string password_hash
        string role
        timestamp created_at
    }
    sites {
        uuid id PK
        uuid org_id FK
        string name
        string address
        geometry footprint
        timestamp created_at
    }
    robots {
        uuid id PK
        uuid org_id FK
        uuid site_id FK
        string name
        string serial_number
        string status
        int battery_pct
        timestamp last_seen_at
    }
    sessions {
        uuid id PK
        uuid site_id FK
        uuid robot_id FK
        uuid operator_id FK
        string mode
        string status
        string video_storage_key
        timestamp started_at
        timestamp ended_at
    }
    meshes {
        uuid id PK
        uuid session_id FK
        uuid previous_mesh_id FK
        string visibility
        int version
        string object_storage_key
        string tileset_key
        string preview_key
        timestamp created_at
    }
    annotations {
        uuid id PK
        uuid mesh_id FK
        uuid author_id FK
        geometry position
        string label
        string severity
        text notes
        timestamp created_at
    }
    measurements {
        uuid id PK
        uuid mesh_id FK
        uuid author_id FK
        string type
        jsonb points
        float value_meters
        timestamp created_at
    }
    reports {
        uuid id PK
        uuid session_id FK
        uuid author_id FK
        string title
        string pdf_storage_key
        timestamp generated_at
    }
    scheduled_inspections {
        uuid id PK
        uuid site_id FK
        uuid robot_id FK
        timestamp run_at
        string status
    }
    inspection_jobs {
        uuid id PK
        uuid schedule_id FK
        uuid session_id FK
        string status
    }
    storage_objects {
        uuid id PK
        uuid org_id FK
        uuid session_id FK "nullable"
        string bucket
        string object_key UK "matches the *_key columns"
        string kind "RGB_SET | DEPTH_SET | TELEMETRY | SLAM_MAP | VIDEO | MESH | TILESET | PREVIEW | REPORT | INTERMEDIATE"
        string content_type
        bigint size_bytes
        string sha256
        string status "PENDING | AVAILABLE | ARCHIVED | DELETED"
        timestamp expires_at "from retention policy"
        timestamp created_at
    }

    organizations ||--o{ users : has
    organizations ||--o{ sites : owns
    organizations ||--o{ robots : owns
    sites ||--o| robots : assigned
    users ||--o{ sessions : operates
    sites ||--o{ sessions : hosts
    robots ||--o{ sessions : performs
    sessions ||--o| meshes : produces
    sessions ||--o| reports : generates
    meshes |o--o| meshes : previous_version
    meshes ||--o{ annotations : has
    meshes ||--o{ measurements : has
    users ||--o{ annotations : authors
    users ||--o{ measurements : authors
    sites ||--o{ scheduled_inspections : schedules
    robots ||--o{ scheduled_inspections : target
    scheduled_inspections ||--o| inspection_jobs : creates
    inspection_jobs ||--o| sessions : results_in
    organizations ||--o{ storage_objects : "owns files"
    sessions |o--o{ storage_objects : "raw capture, video"
Loading

Why storage_objects is added

The existing *_key columns already say where each file is. storage_objects adds what those columns can't:

Need How storage_objects covers it
Capacity tracking size_bytes per file → total usage per organization, site or session with one query
Upload status status shows whether an upload is pending, finished, archived or deleted
Integrity sha256 checks that a file arrived undamaged
Retention and cleanup expires_at + status drive the lifecycle process (section 3)
Raw capture link Raw photos, depth frames, telemetry and SLAM maps have no column in the existing tables. session_id + kind links them to their session

Indexes on storage_objects

  • UNIQUE (bucket, object_key).
  • (session_id, kind) to find all files of a session.
  • (status, expires_at) for the retention sweeper.

2. Object Storage — ER Diagram

Object storage has no tables or foreign keys. Its structure comes from buckets and key prefixes (folders). The diagram shows that structure as entities. Each "attribute" is the key pattern, file format or object metadata.

Buckets

Bucket Content Access Lifecycle
roamview-raw Raw capture from the robot: RGB frames, depth frames, telemetry bag, SLAM map Write: Ingestion Service. Read: Processing Service only Standard tier for 30 days → cold/infrequent-access tier → delete at 365 days
roamview-work Processing intermediates (COLMAP sparse model, OpenMVS dense cloud) Processing Service only Delete after 7 days
roamview-assets Client-facing 3D data: mesh, tileset + tiles, preview image Read by clients through signed URLs / CDN Kept while the mesh is kept (365 days by default, see SP-GU05.03)
roamview-media Session video recordings, inspection report PDFs Read by clients through signed URLs Video: 90 days. Reports: 365 days

Keys use IDs, never names. Renaming a site does not move files. The orgs/{org_id}/ prefix makes per-tenant access policies and usage totals simple.

erDiagram
    direction TB
    BUCKET {
        string name "roamview-raw | -work | -assets | -media"
        string region
        string encryption "SSE (AES-256)"
        string lifecycle_rules
    }
    ORG_PREFIX {
        string key "orgs/{org_id}/"
    }
    SITE_PREFIX {
        string key "orgs/{org_id}/sites/{site_id}/"
    }
    SESSION_PREFIX {
        string key "…/sessions/{session_id}/"
    }
    CAPTURE_MANIFEST {
        string key "…/capture/manifest.json"
        json frames "seq, file, timestamp, pose, sha256"
        string meta "x-amz-meta-session-id"
    }
    RGB_FRAME {
        string key "…/capture/rgb/{seq:06d}.jpg"
        string format "JPEG 3840x2160"
        float size_mb "~2.5"
    }
    DEPTH_FRAME {
        string key "…/capture/depth/{seq:06d}.png"
        string format "PNG uint16 640x480"
        float size_mb "~0.3"
    }
    TELEMETRY_BAG {
        string key "…/telemetry/session.mcap"
        string format "ROS 2 MCAP (LiDAR, IMU, odom, tf)"
        float size_mb "~0.05 per second"
    }
    SLAM_MAP {
        string key "…/slam/map.pgm + map.yaml"
        float size_mb "~1"
    }
    VIDEO_RECORDING {
        string key "…/video/recording.mp4"
        string format "H.264 720p30 ~2.5 Mbps"
    }
    WORK_PREFIX {
        string key "…/sessions/{session_id}/work/"
    }
    SPARSE_MODEL {
        string key "…/sparse/ (cameras, images, points3D)"
    }
    DENSE_CLOUD {
        string key "…/dense/scene_dense.ply"
    }
    MESH_VERSION_PREFIX {
        string key "…/meshes/{mesh_id}/v{version}/"
    }
    MESH_FILE {
        string key "…/mesh.glb"
        string format "glTF binary, textured"
        float size_mb "~150"
    }
    TILESET_ROOT {
        string key "…/tiles/tileset.json"
        string format "3D Tiles 1.1"
    }
    TILE {
        string key "…/tiles/{level}/{x}_{y}_{z}.glb"
        string format "glTF content per LOD node"
    }
    PREVIEW {
        string key "…/preview.webp"
    }
    REPORT_PDF {
        string key "orgs/{org_id}/reports/{report_id}.pdf"
    }

    BUCKET ||--o{ ORG_PREFIX : contains
    ORG_PREFIX ||--o{ SITE_PREFIX : contains
    ORG_PREFIX ||--o{ REPORT_PDF : "contains (media)"
    SITE_PREFIX ||--o{ SESSION_PREFIX : "contains (raw, media, work)"
    SITE_PREFIX ||--o{ MESH_VERSION_PREFIX : "contains (assets)"
    SESSION_PREFIX ||--o| CAPTURE_MANIFEST : "raw"
    CAPTURE_MANIFEST ||--|{ RGB_FRAME : lists
    CAPTURE_MANIFEST ||--o{ DEPTH_FRAME : lists
    SESSION_PREFIX ||--o| TELEMETRY_BAG : "raw"
    SESSION_PREFIX ||--o| SLAM_MAP : "raw"
    SESSION_PREFIX ||--o| VIDEO_RECORDING : "media"
    SESSION_PREFIX ||--o| WORK_PREFIX : "work"
    WORK_PREFIX ||--o| SPARSE_MODEL : contains
    WORK_PREFIX ||--o| DENSE_CLOUD : contains
    MESH_VERSION_PREFIX ||--|| MESH_FILE : contains
    MESH_VERSION_PREFIX ||--o| TILESET_ROOT : contains
    TILESET_ROOT ||--|{ TILE : references
    MESH_VERSION_PREFIX ||--o| PREVIEW : contains
Loading

3D tiles

A tileset can hold hundreds or thousands of tile files. A database row per tile would add rows without adding anything the API needs. The client only needs the root tileset.json, and Cesium follows its references to fetch tiles. For that reason:

  • meshes.tileset_key points at the root tileset.json, as in the existing ER diagram.
  • storage_objects has one row of kind TILESET for that root. Its size_bytes is the total of the whole tiles/ prefix.
  • A signed URL (or CDN signed cookie) is issued for the tiles/ prefix so the client can fetch child tiles.

The raw photo set follows the same approach: one manifest.json row, not one row per frame.


3. Links Between Metadata and Object Storage

flowchart LR
    subgraph PG["PostgreSQL (metadata)"]
        SES["sessions<br/>video_storage_key"]
        MSH["meshes<br/>object_storage_key · tileset_key · preview_key"]
        REP["reports<br/>pdf_storage_key"]
        SO[("storage_objects<br/>bucket + object_key<br/>size, sha256, status")]
    end
    subgraph OS["Object storage"]
        RAW["roamview-raw<br/>capture/manifest.json · rgb/* · depth/*<br/>telemetry/session.mcap · slam/map.*"]
        WORK["roamview-work<br/>work/sparse · dense"]
        AST["roamview-assets<br/>meshes/{id}/v{n}/mesh.glb · tiles/* · preview.webp"]
        MED["roamview-media<br/>video/recording.mp4 · reports/{id}.pdf"]
    end
    SES -- "key = object_key" --> SO
    MSH -- "key = object_key" --> SO
    REP -- "key = object_key" --> SO
    SES -- "session_id (raw capture)" --> SO
    SO -. "(bucket, object_key)" .-> RAW
    SO -. "(bucket, object_key)" .-> WORK
    SO -. "(bucket, object_key)" .-> AST
    SO -. "(bucket, object_key)" .-> MED
Loading

Links in both directions:

  • Metadata → object: a domain row's *_key column holds the object key. The same key appears in storage_objects.object_key, which adds size, checksum and status. Raw capture files are found through storage_objects.session_id.
  • Object → metadata: every object is uploaded with the headers x-amz-meta-storage-object-id, x-amz-meta-org-id and x-amz-meta-session-id. The key prefix also contains the org, site and session IDs. An orphaned file can therefore be traced back to its rows without a database lookup.

Upload and read lifecycle

sequenceDiagram
    autonumber
    participant R as Robot (Pi 5)
    participant I as Ingestion Service
    participant DB as PostgreSQL
    participant S3 as Object Storage
    participant P as Processing Service
    participant C as Web / Mobile client

    R->>I: POST /sessions/{id}/uploads (kind, size, sha256)
    I->>DB: INSERT storage_objects (status = PENDING)
    I-->>R: pre-signed PUT URL(s)
    R->>S3: PUT objects (multipart, resumable)
    R->>I: POST /uploads/{object_id}/complete
    I->>S3: HEAD object (check size + checksum)
    I->>DB: UPDATE storage_objects SET status = AVAILABLE<br/>UPDATE sessions SET video_storage_key, status = PROCESSING
    I->>P: enqueue processing for session
    P->>S3: GET raw capture, PUT intermediates + mesh + tiles
    P->>DB: INSERT storage_objects + meshes (object_storage_key,<br/>tileset_key, preview_key) in one transaction<br/>UPDATE sessions SET status = READY
    C->>DB: GET /sites/{id}/meshes (via API, auth + visibility check)
    DB-->>C: mesh metadata + short-lived signed URL
    C->>S3: GET tileset.json → tiles (via CDN)
Loading

Consistency rules

  1. Upload first, then commit. A *_key column is only filled once the matching storage_objects row is AVAILABLE. PENDING rows older than 24 h are marked failed, and their partial uploads are aborted.
  2. Orphan sweep (daily). Objects without an AVAILABLE/ARCHIVED registry row are deleted. Registry rows whose object is missing are flagged and reported.
  3. Deletion is status-driven. Retention sets expires_at. The sweeper deletes the object, then sets status = DELETED. The row is kept for audit and usage history.
  4. Privacy. Every bucket is private and encrypted at rest. Clients never get bucket credentials, only signed URLs that expire within minutes. PUBLIC meshes skip the ownership check but still use signed URLs. See Ethics & Accountability.

4. Capacity Estimation

All figures are estimates from stated assumptions. They should be revisited when hardware tests (Hardware Testing) produce measured values. Units: 1 GB = 1000 MB. Script used: capacity.py.

4.1 Assumptions

Parameter Value Source / rationale
Autonomous inspection length 30 min (1800 s) ~6 rooms × T_room = 300 s (SP-GU01.03)
Live walkthrough length 6 min (360 s) T_max control lease (SP-GU01.03)
Photogrammetry keyframe rate 0.5 fps → 900 frames / inspection 1 frame every 2 s gives enough overlap for COLMAP at walking speed
RGB keyframe 3840×2160 JPEG, ~2.5 MB OAK-D Lite IMX214 colour sensor (max 4208×3120, 4K30)
Depth frame 640×480 uint16 PNG, ~0.3 MB OAK-D Lite stereo pair; 614 KB raw, ~50% lossless compression
Telemetry bag (MCAP, zstd) ~0.05 MB/s Waveshare D500: 5000 samples/s at 10 Hz → ~40 KB/s raw, plus IMU/odom/tf, ~2:1 compression
Video recording 720p30 H.264, 2.5 Mbps Pi Camera Module 3 Wide. The Pi 5 has no hardware H.264 encoder, so software encoding at 720p is the safe baseline
Intermediate files ~1 GB / inspection COLMAP sparse model + OpenMVS dense point cloud
Textured mesh ~150 MB ~1M faces + textures, .glb
3D tileset 4/3 × mesh ≈ 200 MB Quadtree LOD pyramid: 1 + ¼ + 1⁄16 + … = 4/3
Preview / report PDF 0.5 MB / 5 MB —
Retention Raw 365 d (hot 30 d, then cold) · intermediates 7 d · video 90 d · mesh, tiles, reports 365 d Proposed policy. Mesh retention to be confirmed by SP-GU05.03

4.2 Per-session footprint

Object class Bucket Autonomous (30 min) Live walkthrough (6 min)
RGB photo set (900 frames) raw 2,250 MB —
Depth frames (900) raw 270 MB —
Telemetry bag raw 90 MB 18 MB
SLAM map raw 1 MB —
Video recording media 563 MB 113 MB
Intermediates (temporary) work 1,000 MB —
Mesh assets 150 MB —
3D tileset assets 200 MB —
Preview + report assets / media 6 MB —
Total ≈ 4.5 GB ≈ 0.13 GB

The raw RGB photo set is ~50% of each inspection and ~70% of long-term storage. Its retention period is the main factor in the total.

4.3 Growth scenarios (steady state after 12 months)

Scenario Sites Sessions / site / week (auto + live) Sessions / year Ingest / month Hot tier Cold tier Total Total + 20% headroom
A. Capstone demo 1 2 + 3 104 + 156 40 GB 81 GB 252 GB 333 GB 400 GB
B. Pilot 10 1 + 5 521 + 2,607 222 GB 456 GB 1.3 TB 1.75 TB 2.1 TB
C. Growth 100 3 + 10 15,643 + 52,143 6.4 TB 12.9 TB 38.3 TB 51 TB 62 TB

Steady state for each class = daily ingest × retention days. Hot tier = everything except raw capture older than 30 days.

Sensitivity: shortening raw retention from 365 to 90 days reduces Scenario B from 1.75 TB to ≈ 0.69 TB. Reprocessing is then only possible within 90 days.

4.4 PostgreSQL (metadata)

Item Estimate
Rows per autonomous inspection ~46 (1 session, 1 mesh, ~11 storage objects, ~20 annotations, ~10 measurements, 1 report, 1 schedule, 1 job)
Rows per live walkthrough ~3 (1 session, 2 storage objects)
Average row size (with indexes) ~300 B × 2
Year-1 size A: ~3 MB · B: ~19 MB · C: ~0.5 GB

The metadata database is small at every scenario. One managed PostgreSQL instance with the smallest storage tier (e.g. 20 GB) and daily backups is enough. The design keeps it small because frames and tiles are not stored as rows.

4.5 Robot local buffer and upload

Item Estimate
Data to upload per autonomous inspection (raw + video) ~3.2 GB
Upload time at 20 Mbps effective Wi-Fi uplink ~21 min (an input for SP-GU08.02, "Save Video Recording Job Max Time")
Local storage needed 64 GB microSD/NVMe minus ~16 GB OS/ROS → buffer for ~15 inspections if Wi-Fi is down. Local copies are deleted after the upload is confirmed (status = AVAILABLE)

Component Diagram

The Processing Service turns the raw capture of a finished session (RGB keyframes + depth + telemetry) into a textured mesh and a Cesium 3D tileset. It writes them back to object storage and registers them in PostgreSQL (see Data Model & Storage).

flowchart LR
    ING["«component»<br/>Ingestion Service"]
    Q[("«component»<br/>Job Queue")]
    DB[("«component»<br/>PostgreSQL")]
    S3[("«component»<br/>Object Storage")]
    API["«component»<br/>API Gateway"]

    subgraph PS["«component» Processing Service"]
        direction TB
        JC["«component»<br/>Job Consumer<br/>pull · heartbeat · retry"]
        ORC["«component»<br/>Pipeline Orchestrator<br/>runs stages in order,<br/>updates job status"]

        subgraph STG["Pipeline Stages (IStage)"]
            direction TB
            PREP["«component»<br/>Capture Preparation<br/>verify manifest + sha256,<br/>select keyframes"]
            SFM["«component»<br/>SfM Stage — COLMAP<br/>feature_extractor → sequential_matcher<br/>→ mapper → image_undistorter"]
            MVS["«component»<br/>MVS Stage — OpenMVS<br/>InterfaceCOLMAP → DensifyPointCloud<br/>→ ReconstructMesh → RefineMesh → TextureMesh"]
            EXP["«component»<br/>Export Stage<br/>mesh → mesh.glb,<br/>render preview.webp"]
            TIL["«component»<br/>Tiling Stage<br/>mesh.glb → tileset.json + tiles/*.glb"]
            PREP --> SFM --> MVS --> EXP --> TIL
        end

        SA["«component»<br/>Storage Adapter<br/>S3 download / upload"]
        MR["«component»<br/>Metadata Repository<br/>sessions · meshes · storage_objects"]
        WS[("«artifact»<br/>Local scratch workspace<br/>/work/{session_id}")]

        JC --> ORC
        ORC --> STG
        STG --> WS
        ORC --> SA
        ORC --> MR
    end

    ING -- "IJobPublisher<br/>enqueue(session_id)" --> Q
    Q -- "IJobSource<br/>next() / ack() / fail()" --> JC
    SA -- "IObjectStore<br/>S3 API (HTTPS)" --> S3
    MR -- "IMetadataRepo<br/>SQL (TLS)" --> DB
    ORC -- "INotifier<br/>session READY / FAILED" --> API
Loading

Components

Component Responsibility Interfaces
Job Consumer Takes one job at a time from the queue. Sends a heartbeat while the job runs. Retries failed jobs up to 3 times, then gives up Uses IJobSource
Pipeline Orchestrator Runs the stages in order, times each stage, updates the job status and sessions.status, and cleans the scratch workspace Uses IStage, IObjectStore, IMetadataRepo, INotifier
Capture Preparation Downloads the capture set using manifest.json, checks each file's checksum and drops blurry or duplicate frames Implements IStage
SfM Stage (COLMAP) Estimates camera positions and a sparse point cloud, then produces undistorted images for OpenMVS Implements IStage
MVS Stage (OpenMVS) Builds the dense point cloud, the mesh and the texture Implements IStage
Export Stage Converts the textured mesh to mesh.glb (TransformScene) and renders a preview image Implements IStage
Tiling Stage Splits mesh.glb into a 3D Tiles 1.1 tileset with levels of detail (e.g. 3d-tiles-tools / py3dtiles) Implements IStage
Storage Adapter Reads from and writes to the S3 buckets and adds the object metadata headers Provides IObjectStore
Metadata Repository Writes storage_objects rows, inserts the meshes row and updates sessions.status, all in one transaction Provides IMetadataRepo

Pipeline stages and outputs

# Stage Tool / commands Input Output Written to
1 Capture preparation Python capture/manifest.json, rgb/*.jpg, depth/*.png Checked frame list Scratch
2 Sparse reconstruction (SfM) COLMAP feature_extractor, sequential_matcher, mapper, image_undistorter Keyframes sparse/ (cameras, images, points3D), undistorted images roamview-work/…/work/sparse/
3 Dense reconstruction OpenMVS InterfaceCOLMAP, DensifyPointCloud COLMAP dense folder scene.mvs, scene_dense.ply roamview-work/…/work/dense/
4 Meshing + texturing OpenMVS ReconstructMesh, RefineMesh, TextureMesh scene_dense.mvs Textured mesh (.ply + texture) Scratch
5 Export OpenMVS TransformScene + preview render Textured mesh mesh.glb, preview.webp roamview-assets/…/meshes/{mesh_id}/v{n}/
6 Tiling 3D Tiles tool mesh.glb tiles/tileset.json, tiles/{level}/*.glb roamview-assets/…/meshes/{mesh_id}/v{n}/tiles/
7 Register Metadata Repository Object keys + sizes meshes row (object_storage_key, tileset_key, preview_key), storage_objects rows, sessions.status = READY PostgreSQL

Deployment Diagram

flowchart LR
    subgraph EDGE["On site / user devices"]
        direction TB
        ROBOT["«device» Robot — Raspberry Pi 5 (4 GB)<br/>«execution env» Ubuntu + ROS 2<br/>«artifact» ROS 2 nodes (SLAM · Nav2 · camera)<br/>«artifact» Session uploader<br/>«artifact» Local buffer 64 GB"]
        CLIENT["«device» Client<br/>«execution env» Web browser / iOS / Android<br/>«artifact» React app · React Native app<br/>(Three.js / Cesium viewer)"]
    end

    subgraph CLOUD["«cloud» RoamView cloud (VPC)"]
        direction LR
        subgraph APPNODE["«device» App server (CPU)"]
            direction TB
            GW["«container» API Gateway"]
            IOT["«container» IoT Gateway"]
            STR["«container» Streaming Service"]
            INGC["«container» Ingestion Service (Node.js)"]
        end

        subgraph DATA["Data tier"]
            direction TB
            REDIS[("«device» Queue node<br/>«container» Redis")]
            PG[("«device» Managed PostgreSQL")]
            OBJ[("«device» S3-compatible object storage<br/>raw · work · assets · media")]
            CDN["«device» CDN edge"]
        end

        GPU["«device» GPU processing node (autoscaled 0…N)<br/>«execution env» Docker + NVIDIA Container Toolkit (CUDA)<br/>«container» roamview-processor<br/>«artifact» Processing Service: COLMAP · OpenMVS · 3D Tiles tool<br/>«artifact» Scratch NVMe ≥ 100 GB"]
    end

    ROBOT -- "WSS / MQTT-TLS: control, telemetry, video" --> IOT
    ROBOT -- "HTTPS REST: upload session" --> INGC
    ROBOT -- "HTTPS pre-signed PUT" --> OBJ
    CLIENT -- "HTTPS / WSS" --> GW
    CLIENT -- "HTTPS signed URLs" --> CDN
    CDN --> OBJ
    IOT --> STR
    GW --> STR
    GW --> IOT
    GW -- "SQL/TLS" --> PG
    INGC -- "SQL/TLS" --> PG
    INGC -- "enqueue" --> REDIS
    REDIS -- "dequeue" --> GPU
    GPU -- "S3 API / HTTPS" --> OBJ
    GPU -- "SQL/TLS" --> PG
    GPU -- "status callback HTTPS" --> GW
Loading

Nodes

Node Runs Notes
Robot (Raspberry Pi 5) ROS 2 nodes, session uploader Buffers about 15 inspections locally if Wi-Fi drops. Uploads straight to object storage using pre-signed URLs
Client React web app, React Native app Loads meshes and tiles from the CDN using signed URLs
App server API Gateway, IoT Gateway, Streaming Service, Ingestion Service CPU only. Always on
Queue node Redis Holds processing jobs between the Ingestion Service and the GPU workers
Managed PostgreSQL Metadata DB Schema in Data Model & Storage
Object storage + CDN Four buckets Private buckets. Encrypted at rest
GPU processing node roamview-processor Docker image Starts when jobs are waiting and shuts down when the queue is empty, so the team pays for the GPU only while it's working

GPU / hardware requirements (GPU processing node)

Resource Minimum Recommended Why
GPU NVIDIA, CUDA-capable, 8 GB VRAM 16 GB VRAM (e.g. T4 / RTX-class) COLMAP SIFT extraction and matching run much faster on GPU. OpenMVS DensifyPointCloud has an optional CUDA build. COLMAP's own dense stereo requires CUDA, but the dense step here uses OpenMVS, so a CPU-only fallback is possible, just much slower
CPU 8 vCPU 16 vCPU Meshing and texturing (ReconstructMesh, TextureMesh) run mostly on the CPU
RAM 32 GB 64 GB Dense reconstruction of about 900 4K frames. Can be lowered with --resolution-level 1 (half resolution)
Scratch disk 100 GB NVMe 200 GB NVMe Raw capture (~2.5 GB), undistorted images, depth maps, dense cloud, mesh
Software Docker, NVIDIA driver + Container Toolkit, CUDA 12 — COLMAP, OpenMVS and the tiling tool are packaged in one image

These are estimates for a 30-minute inspection (900 keyframes, see the capacity section of Data Model & Storage). They should be confirmed by timing one real run on the chosen GPU node.

Clone this wiki locally