diff --git a/ooad-docs/diagrams/Class Diagram WalkGuide RILLL FIX RIL.drawio.pdf b/ooad-docs/diagrams/Class Diagram WalkGuide RILLL FIX RIL.drawio.pdf new file mode 100644 index 0000000..75d4bb7 Binary files /dev/null and b/ooad-docs/diagrams/Class Diagram WalkGuide RILLL FIX RIL.drawio.pdf differ diff --git a/ooad-docs/diagrams/Er Diagram WalkGuide RILL FIX.drawio.pdf b/ooad-docs/diagrams/Er Diagram WalkGuide RILL FIX.drawio.pdf new file mode 100644 index 0000000..8d2f5ef Binary files /dev/null and b/ooad-docs/diagrams/Er Diagram WalkGuide RILL FIX.drawio.pdf differ diff --git a/ooad-docs/diagrams/component-diagram.puml b/ooad-docs/diagrams/component-diagram.puml index c3d8914..8f744cd 100644 --- a/ooad-docs/diagrams/component-diagram.puml +++ b/ooad-docs/diagrams/component-diagram.puml @@ -1,35 +1,542 @@ -@startuml +@startuml WalkGuide_Component_Diagram +title Component Diagram — WalkGuide System (v2.0) + skinparam componentStyle rectangle - -component "Flutter Mobile App" { - [Presentation Screens] - [ApiClient + Interceptors] - [TTS/STT/YOLO/Location Services] - [WebSocketService] - [CallService] +skinparam backgroundColor #FAFAFA +skinparam component { + BackgroundColor #EEF2FF + BorderColor #6366F1 + FontColor #1E1B4B + FontSize 11 +} +skinparam package { + BackgroundColor #F0F4FF + BorderColor #818CF8 + FontColor #1E1B4B + FontSize 12 + FontStyle bold +} +skinparam interface { + BackgroundColor #DBEAFE + BorderColor #3B82F6 +} +skinparam database { + BackgroundColor #FEF3C7 + BorderColor #D97706 +} +skinparam cloud { + BackgroundColor #F0FDF4 + BorderColor #16A34A +} +skinparam arrow { + Color #4B5563 + FontSize 9 + FontColor #374151 +} +skinparam note { + BackgroundColor #FFFBEB + BorderColor #F59E0B + FontSize 9 } -component "Spring Boot Backend" { - [Controllers] - [Services] - [Repositories] - [Security/JWT] - [WebSocket Broker] +' ───────────────────────────────────────────── +' MOBILE DEVICE — Flutter App +' ───────────────────────────────────────────── +package "Mobile Device (Flutter App)" as FLUTTER #E8EAF6 { + + package "Presentation Layer" as PRESENTATION { + [ServerConnectScreen] as SCS + [SplashScreen] as SS + [LoginScreen] as LS + [RegisterScreen] as RS + + package "User Screens" as US_SCREENS { + [WalkGuideScreen] as WGS + [SosScreen] as SOSS + [ActivityLogScreen] as ALS + [NotificationScreen] as NS + [NavigationModeScreen] as NMS + [UserSettingsScreen] as USS + [CallScreen] as CS + [ManualScreen] as MS + [UserPairingScreen] as UPS + } + + package "Guardian Screens" as GS_SCREENS { + [GuardianDashboardScreen] as GDS + [GuardianMapScreen] as GMS + [GuardianActivityLogScreen] as GALS + [GuardianSendNotifScreen] as GSNS + [GuardianAiConfigScreen] as GACS + [GuardianVoiceCmdScreen] as GVCS + [GuardianShortcutScreen] as GSHS + [GuardianGeofenceScreen] as GGFS + [GuardianPairingScreen] as GPS + [IncomingCallScreen] as ICS + } + } + + package "Application Layer (BLoC / Cubit)" as BLOC_LAYER { + [AuthBloc] as AuthBloc + [PairingBloc] as PairingBloc + [WalkGuideBloc] as WGBloc + [SosBloc] as SosBloc + [LocationBloc] as LocBloc + [NotificationBloc] as NotifBloc + [ActivityLogBloc] as ActLogBloc + [VoiceCommandBloc] as VCBloc + [ServerConnectBloc] as SCBloc + [CallBloc] as CallBloc + [GuardianDashboardBloc] as GDBloc + } + + package "Domain Layer" as DOMAIN { + package "Use Cases" as USECASES { + [LoginUseCase] as LoginUC + [RegisterUseCase] as RegUC + [LogoutUseCase] as LogoutUC + [InviteUserUseCase] as InviteUC + [RespondPairingUseCase] as RespUC + [UnpairUseCase] as UnpairUC + [StartSessionUseCase] as StartUC + [StopSessionUseCase] as StopUC + [LogObstacleUseCase] as LogObsUC + [TriggerSosUseCase] as TrigSosUC + [UpdateLocationUseCase] as UpdLocUC + } + package "Repository Interfaces" as REPO_INTF { + interface "IAuthRepository" as IAuthRepo + interface "IPairingRepository" as IPairRepo + interface "IWalkGuideRepository" as IWGRepo + interface "IActivityLogRepository" as IActRepo + interface "ISosRepository" as ISosRepo + interface "ILocationRepository" as ILocRepo + interface "INotificationRepository" as INotifRepo + interface "IAiConfigRepository" as IAIRepo + } + } + + package "Data Layer" as DATA { + package "Repository Implementations" as REPO_IMPL { + [AuthRepositoryImpl] as AuthRepoImpl + [PairingRepositoryImpl] as PairRepoImpl + [WalkGuideRepositoryImpl] as WGRepoImpl + [ActivityLogRepositoryImpl] as ActRepoImpl + [SosRepositoryImpl] as SosRepoImpl + [LocationRepositoryImpl] as LocRepoImpl + [NotificationRepositoryImpl] as NotifRepoImpl + [AiConfigRepositoryImpl] as AIRepoImpl + } + + package "Remote Data Sources" as REMOTE_DS { + [AuthRemoteDataSource] as AuthDS + [WalkGuideRemoteDataSource] as WGDS + [ActivityLogRemoteDataSource] as ActDS + [SosRemoteDataSource] as SosDS + [LocationRemoteDataSource] as LocDS + [NotificationRemoteDataSource] as NotifDS + [GuardianRemoteDataSource] as GrdDS + } + + package "Local Data Sources (Drift/SQLite)" as LOCAL_DS { + [SecureStorageService] as SecStore + [LocalDatabase (Drift)] as DriftDB + [SharedPreferencesService] as SharedPref + } + + package "Core Services (Singletons)" as CORE_SVC { + [TtsService\n(flutter_tts)] as TTS + [SttService\n(speech_to_text)] as STT + [VoiceCommandHandler] as VCH + [HapticService\n(vibration)] as HAP + [HardwareShortcutListener] as HSL + [FcmService\n(firebase_messaging)] as FCMSvc + [WebSocketService\n(stomp_dart_client)] as WSSvc + [AgoraService\n(agora_rtc_engine)] as AgoraSvc + } + + package "AI Engine (On-Device)" as AI_ENGINE { + [ModelLoader\n(tflite_flutter)] as ModelLoader + [YoloDetector\n(YOLOv8n)] as YOLO + [ObstacleAnalyzer] as ObsAnalyzer + } + + [ApiClient\n(Dio + Interceptors)] as DioClient + } } -database "PostgreSQL" as DB -cloud "Firebase FCM" as FCM -cloud "Agora RTC" as Agora -cloud "OpenStreetMap/OSRM" as Maps +' ───────────────────────────────────────────── +' BACKEND SERVER — Spring Boot +' ───────────────────────────────────────────── +package "Backend Server (Spring Boot @ 202.46.28.160:8080)" as BACKEND #E8F5E9 { + + package "Security & Config" as SEC_CFG { + [SecurityConfig\n(CORS, CSRF, RBAC)] as SecConfig + [JwtAuthFilter\n(OncePerRequestFilter)] as JWTFilter + [JwtUtil\n(JJWT)] as JWTUtil + [CustomUserDetailsService] as CUDS + [WebSocketConfig\n(STOMP)] as WSConfig + [GlobalExceptionHandler\n(@ControllerAdvice)] as GEH + [SwaggerConfig\n(Springdoc OpenAPI)] as SwagConfig + } + + package "Controller Layer" as CTRL { + [AuthController\n/api/v1/auth] as AuthCtrl + [PairingController\n/api/v1/shared/pairing] as PairCtrl + [GuardianController\n/api/v1/guardian] as GrdCtrl + [UserController\n/api/v1/user] as UserCtrl + [CallController\n/api/v1/shared/call] as CallCtrl + } + + package "Service Layer" as SVC { + [AuthService] as AuthSvc + [PairingService] as PairSvc + [ActivityLogService] as ActLogSvc + [LocationService\n(+ Haversine)] as LocSvc + [ObstacleLogService] as ObsLogSvc + [NotificationService] as NotifSvc + [SosService] as SosSvc + [AiConfigService] as AICfgSvc + [VoiceCommandService] as VCSvc + [HardwareShortcutService] as HWSvc + [GeofenceService\n(Haversine)] as GeoSvc + [UserSettingsService] as UserSetSvc + [FcmService\n(Firebase Admin)] as FcmBackSvc + [AgoraTokenService] as AgoraSvcBE + [GuardianDashboardService] as GDSvc + } + + package "WebSocket Broadcaster" as WS_BROADCAST { + [LocationBroadcaster\n(SimpMessagingTemplate)] as LocBcast + } + + package "Repository Layer (Spring Data JPA)" as REPO { + [UserRepository] as UserRepo + [PairingRelationRepository] as PairRepo + [ActivityLogRepository] as ActRepo + [ObstacleLogRepository] as ObsRepo + [LocationHistoryRepository] as LocRepo + [GuardianNotificationRepository] as NotifRepo + [SosEventRepository] as SosRepo + [UserSettingsRepository] as UserSetRepo + [AiConfigRepository] as AIRepo + [VoiceCommandConfigRepository] as VCRepo + [HardwareShortcutRepository] as HWRepo + [GeofenceConfigRepository] as GeoRepo + [RefreshTokenRepository] as RTRepo + } + + package "Domain / Entity" as ENTITY { + [User] as UEnt + [PairingRelation] as PRel + [ActivityLog] as ActEnt + [ObstacleLog] as ObsEnt + [LocationHistory] as LocEnt + [GuardianNotification] as NotifEnt + [SosEvent] as SosEnt + [UserSettings] as UserSetEnt + [AiConfig] as AICfgEnt + [VoiceCommandConfig] as VCEnt + [HardwareShortcut] as HWEnt + [GeofenceConfig] as GeoEnt + [RefreshToken] as RTEnt + } +} + +' ───────────────────────────────────────────── +' EXTERNAL SERVICES +' ───────────────────────────────────────────── +package "External Services" as EXTERNAL #FFF8E1 { + cloud "Firebase (FCM)\nPush Notifications" as FCM + cloud "Agora RTC\nVoIP Audio Call" as AGORA + cloud "OpenStreetMap\n(flutter_map tiles)" as OSM +} + +database "PostgreSQL\n202.46.28.160:2002\n(University Server)\nFlyway Migrations V1-V16" as PGDB + +' ───────────────────────────────────────────── +' PRESENTATION → BLOC +' ───────────────────────────────────────────── +SCS --> SCBloc +SS --> AuthBloc +LS --> AuthBloc +RS --> AuthBloc +WGS --> WGBloc +WGS --> VCBloc +SOSS --> SosBloc +ALS --> ActLogBloc +NS --> NotifBloc +NMS --> LocBloc +USS --> AuthBloc +CS --> CallBloc +UPS --> PairingBloc +GDS --> GDBloc +GMS --> LocBloc +GALS --> ActLogBloc +GSNS --> NotifBloc +GACS --> AIRepoImpl +GVCS --> VCBloc +GSHS --> VCBloc +GGFS --> GDBloc +GPS --> PairingBloc +ICS --> CallBloc + +' ───────────────────────────────────────────── +' BLOC → USE CASES +' ───────────────────────────────────────────── +AuthBloc --> LoginUC +AuthBloc --> RegUC +AuthBloc --> LogoutUC +PairingBloc --> InviteUC +PairingBloc --> RespUC +PairingBloc --> UnpairUC +WGBloc --> StartUC +WGBloc --> StopUC +WGBloc --> LogObsUC +SosBloc --> TrigSosUC +LocBloc --> UpdLocUC +CallBloc --> AgoraSvc +VCBloc --> VCH + +' ───────────────────────────────────────────── +' USE CASES → REPOSITORY INTERFACES +' ───────────────────────────────────────────── +LoginUC --> IAuthRepo +RegUC --> IAuthRepo +LogoutUC --> IAuthRepo +InviteUC --> IPairRepo +RespUC --> IPairRepo +UnpairUC --> IPairRepo +StartUC --> IWGRepo +StopUC --> IWGRepo +LogObsUC --> IWGRepo +TrigSosUC --> ISosRepo +UpdLocUC --> ILocRepo + +' ───────────────────────────────────────────── +' INTERFACES → IMPLEMENTATIONS +' ───────────────────────────────────────────── +IAuthRepo <|.. AuthRepoImpl +IPairRepo <|.. PairRepoImpl +IWGRepo <|.. WGRepoImpl +IActRepo <|.. ActRepoImpl +ISosRepo <|.. SosRepoImpl +ILocRepo <|.. LocRepoImpl +INotifRepo <|.. NotifRepoImpl +IAIRepo <|.. AIRepoImpl + +' ───────────────────────────────────────────── +' REPO IMPL → DATA SOURCES +' ───────────────────────────────────────────── +AuthRepoImpl --> AuthDS +AuthRepoImpl --> SecStore +PairRepoImpl --> GrdDS +WGRepoImpl --> WGDS +WGRepoImpl --> DriftDB +ActRepoImpl --> ActDS +SosRepoImpl --> SosDS +LocRepoImpl --> LocDS +NotifRepoImpl --> NotifDS +NotifRepoImpl --> DriftDB +AIRepoImpl --> GrdDS + +' ───────────────────────────────────────────── +' REMOTE DATA SOURCES → DioClient +' ───────────────────────────────────────────── +AuthDS --> DioClient +WGDS --> DioClient +ActDS --> DioClient +SosDS --> DioClient +LocDS --> DioClient +NotifDS --> DioClient +GrdDS --> DioClient + +' ───────────────────────────────────────────── +' CORE SERVICES INTERCONNECT +' ───────────────────────────────────────────── +WGBloc --> YOLO +WGBloc --> TTS +WGBloc --> HAP +YOLO --> ModelLoader +YOLO --> ObsAnalyzer +ObsAnalyzer --> TTS +ObsAnalyzer --> HAP +VCH --> STT +VCH --> DriftDB +HSL --> DriftDB +FCMSvc --> DriftDB + +SharedPref --> DioClient : baseUrl + +' ───────────────────────────────────────────── +' DioClient → Backend REST API +' ───────────────────────────────────────────── +DioClient -down-> AuthCtrl : "POST /api/v1/auth/register\nPOST /api/v1/auth/login\nPOST /api/v1/auth/refresh\nPOST /api/v1/auth/logout\nPUT /api/v1/auth/fcm-token\nGET /api/v1/auth/ping" +DioClient -down-> PairCtrl : "POST /shared/pairing/invite\nPOST /shared/pairing/respond\nDELETE /shared/pairing/unpair\nGET /shared/pairing/status" +DioClient -down-> UserCtrl : "GET /user/profile\nPOST /user/location\nPOST /user/obstacle\nPOST /user/sos\nGET /user/notifications\nPOST /user/walkguide/start\nPOST /user/walkguide/stop" +DioClient -down-> GrdCtrl : "GET /guardian/dashboard\nGET /guardian/user-location\nPOST /guardian/notifications/send\nPUT /guardian/ai-config\nPUT /guardian/geofence" +DioClient -down-> CallCtrl : "POST /shared/call/token\nPOST /shared/call/notify" + +' WebSocket STOMP +WSSvc -down-> WSConfig : "ws://host:8080/ws\n(STOMP)\n/topic/location/{userId}\n/queue/sos/{guardianId}\n/queue/notif/{userId}" + +' ───────────────────────────────────────────── +' BACKEND SECURITY FLOW +' ───────────────────────────────────────────── +JWTFilter --> JWTUtil : validate token +JWTFilter --> CUDS : load user +JWTFilter -up-> SecConfig : filter chain +SecConfig -right-> JWTFilter + +' ───────────────────────────────────────────── +' CONTROLLERS → SERVICES +' ───────────────────────────────────────────── +AuthCtrl --> AuthSvc +PairCtrl --> PairSvc +GrdCtrl --> GDSvc +GrdCtrl --> LocSvc +GrdCtrl --> ActLogSvc +GrdCtrl --> ObsLogSvc +GrdCtrl --> NotifSvc +GrdCtrl --> SosSvc +GrdCtrl --> AICfgSvc +GrdCtrl --> VCSvc +GrdCtrl --> HWSvc +GrdCtrl --> GeoSvc +UserCtrl --> AuthSvc +UserCtrl --> LocSvc +UserCtrl --> ObsLogSvc +UserCtrl --> SosSvc +UserCtrl --> ActLogSvc +UserCtrl --> NotifSvc +UserCtrl --> UserSetSvc +UserCtrl --> VCSvc +UserCtrl --> HWSvc +UserCtrl --> AICfgSvc +CallCtrl --> AgoraSvcBE + +' ───────────────────────────────────────────── +' SERVICES CROSS-DEPENDENCIES +' ───────────────────────────────────────────── +AuthSvc --> PairSvc : seed defaults on register +PairSvc --> VCSvc : seedDefaults() +PairSvc --> HWSvc : seedDefaults() +PairSvc --> AICfgSvc : create config on pair +PairSvc --> FcmBackSvc : send FCM invite/response +NotifSvc --> FcmBackSvc : send FCM notification +SosSvc --> FcmBackSvc : send FCM SOS high-priority +AICfgSvc --> FcmBackSvc : notify settings updated +LocSvc --> GeoSvc : checkGeofence() +LocSvc --> LocBcast : broadcastLocation() +LocSvc --> ActLogSvc : log LOCATION_UPDATE +NotifSvc --> LocBcast : broadcastNotification() +SosSvc --> LocBcast : broadcastSos() +SosSvc --> ActLogSvc : log SOS_TRIGGERED +ActLogSvc --> LocBcast : broadcast to Guardian +GDSvc --> LocSvc +GDSvc --> ActLogSvc +GDSvc --> NotifSvc +GDSvc --> SosSvc + +' ───────────────────────────────────────────── +' SERVICES → REPOSITORIES +' ───────────────────────────────────────────── +AuthSvc --> UserRepo +AuthSvc --> RTRepo +AuthSvc --> ActLogSvc +PairSvc --> PairRepo +PairSvc --> UserRepo +ActLogSvc --> ActRepo +LocSvc --> LocRepo +LocSvc --> PairRepo +ObsLogSvc --> ObsRepo +ObsLogSvc --> ActLogSvc +NotifSvc --> NotifRepo +NotifSvc --> PairRepo +SosSvc --> SosRepo +SosSvc --> PairRepo +AICfgSvc --> AIRepo +VCSvc --> VCRepo +HWSvc --> HWRepo +GeoSvc --> GeoRepo +UserSetSvc --> UserSetRepo +FcmBackSvc --> UserRepo +AgoraSvcBE --> UserRepo + +' ───────────────────────────────────────────── +' REPOSITORIES → ENTITIES (JPA/Hibernate) +' ───────────────────────────────────────────── +UserRepo --> UEnt +PairRepo --> PRel +ActRepo --> ActEnt +ObsRepo --> ObsEnt +LocRepo --> LocEnt +NotifRepo --> NotifEnt +SosRepo --> SosEnt +UserSetRepo --> UserSetEnt +AIRepo --> AICfgEnt +VCRepo --> VCEnt +HWRepo --> HWEnt +GeoRepo --> GeoEnt +RTRepo --> RTEnt + +' ───────────────────────────────────────────── +' ENTITIES → DATABASE +' ───────────────────────────────────────────── +UEnt --> PGDB : JPA/Hibernate +PRel --> PGDB +ActEnt --> PGDB +ObsEnt --> PGDB +LocEnt --> PGDB +NotifEnt --> PGDB +SosEnt --> PGDB +UserSetEnt --> PGDB +AICfgEnt --> PGDB +VCEnt --> PGDB +HWEnt --> PGDB +GeoEnt --> PGDB +RTEnt --> PGDB + +' ───────────────────────────────────────────── +' EXTERNAL SERVICES +' ───────────────────────────────────────────── +FcmBackSvc -right-> FCM : "Firebase Admin SDK\nFirebaseMessaging.send()" +AgoraSvcBE -right-> AGORA : "Generate RTC Token\n(server-side Agora SDK)" +AgoraSvc --> AGORA : "agora_rtc_engine\nJoin Channel (audio-only)" +FCMSvc --> FCM : "firebase_messaging\nReceive push" +GMS --> OSM : "flutter_map\nOpenStreetMap tiles" + +note top of PGDB + Tables (Flyway V1–V16): + users, pairing_relations, + activity_logs, obstacle_logs, + location_history, guardian_notifications, + sos_events, user_settings, ai_configs, + voice_command_configs, hardware_shortcuts, + geofence_configs, refresh_tokens +end note + +note right of AI_ENGINE + On-Device AI (No server call): + YOLOv8n .tflite (640×640) + → detect obstacles + → direction: LEFT/CENTER/RIGHT + → TTS + Haptic alert + NNAPI delegate (Android AI accel.) +end note + +note left of SharedPref + Dynamic Server URL: + User inputs "http://202.46.28.160:8080" + Saved to SharedPreferences + No hardcoded BASE_URL in APK +end note + +note bottom of WSSvc + WebSocket STOMP: + /topic/location/{userId} → Guardian map real-time + /queue/sos/{guardianId} → SOS alert instant + /queue/notif/{userId} → Notification push +end note -[Presentation Screens] --> [ApiClient + Interceptors] -[ApiClient + Interceptors] --> [Controllers] : REST /api/v1 -[WebSocketService] --> [WebSocket Broker] : STOMP /ws -[CallService] --> [Controllers] : /shared/call -[Controllers] --> [Services] -[Services] --> [Repositories] -[Repositories] --> DB -[Services] --> FCM -[CallService] --> Agora -[Presentation Screens] --> Maps @enduml diff --git a/ooad-docs/diagrams/sequence-diagram.puml b/ooad-docs/diagrams/sequence-diagram.puml new file mode 100644 index 0000000..8f18b78 --- /dev/null +++ b/ooad-docs/diagrams/sequence-diagram.puml @@ -0,0 +1,381 @@ +@startuml +skinparam sequenceMessageAlign center +skinparam responseMessageBelowArrow true +skinparam maxMessageSize 150 +skinparam sequence { + ParticipantBackgroundColor LightBlue + ActorBackgroundColor LightGray + LifeLineBorderColor gray + GroupBorderColor DarkGray +} + +title WalkGuide System — Complete Sequence Diagram + +' ========================================== +' PARTICIPANTS +' ========================================== +actor "Tunanetra\n(ROLE_USER)" as User +actor "Pendamping\n(ROLE_GUARDIAN)" as Guardian +participant "Flutter App\n(User Side)" as AppUser +participant "Flutter App\n(Guardian Side)" as AppGuardian +participant "Spring Boot\nBackend" as Backend +participant "JWT Auth\nFilter" as JWT +participant "WebSocket\n(STOMP)" as WS +participant "Firebase FCM" as FCM +participant "Agora RTC" as Agora +participant "YOLOv8n\n(On-Device AI)" as YOLO + +' ============================================================ +' 1. SERVER CONNECT +' ============================================================ +== 1. Server Connect (Pertama Kali Install) == + +User -> AppUser : Buka aplikasi +AppUser -> AppUser : Cek SharedPreferences\n(serverUrl ada?) +alt serverUrl TIDAK ada + AppUser -> User : Tampilkan ServerConnectScreen + User -> AppUser : Input URL server\n(http://202.46.28.160:8080) + AppUser -> Backend : GET /api/v1/health (ping) + alt Server OK + Backend --> AppUser : 200 OK + AppUser -> AppUser : Simpan serverUrl\nke SharedPreferences + AppUser -> User : Redirect ke LoginScreen + else Server Gagal + Backend --> AppUser : Timeout / Error + AppUser -> User : Tampilkan pesan error\n"Server tidak dapat dijangkau" + end +else serverUrl sudah ada + AppUser -> User : Langsung ke SplashScreen +end + +' ============================================================ +' 2. AUTH — REGISTER +' ============================================================ +== 2. Register Akun == + +User -> AppUser : Isi form Register\n(email, password, role) +AppUser -> Backend : POST /api/v1/auth/register\n{email, password, displayName, role} +Backend -> Backend : Validasi @NotBlank, @Email +Backend -> Backend : Cek email unik +Backend -> Backend : BCrypt encode password +alt role = ROLE_USER + Backend -> Backend : Generate uniqueUserId\n(12 char alphanumeric) +end +Backend -> Backend : Simpan user ke DB +Backend -> Backend : Buat default UserSettings +Backend -> Backend : Generate JWT access token (1 jam)\n+ refresh token (30 hari) +Backend -> Backend : Simpan refresh token ke DB +Backend --> AppUser : 201 Created\n{accessToken, refreshToken, role, userId, uniqueUserId} +AppUser -> AppUser : Simpan token ke\nFlutterSecureStorage +AppUser -> Backend : PUT /api/v1/shared/fcm-token\n{fcmToken} +Backend --> AppUser : 200 OK +AppUser -> User : Redirect ke Home Screen\n(berdasarkan role) + +' ============================================================ +' 3. AUTH — LOGIN +' ============================================================ +== 3. Login == + +User -> AppUser : Input email & password +AppUser -> Backend : POST /api/v1/auth/login\n{email, password} +Backend -> Backend : findByEmail → validasi BCrypt +Backend -> Backend : Generate JWT access token\n+ refresh token +Backend -> Backend : Simpan refresh token ke DB +Backend -> Backend : Log ActivityLog: LOGIN +Backend --> AppUser : 200 OK\n{accessToken, refreshToken, role, userId} +AppUser -> AppUser : Simpan token ke\nFlutterSecureStorage +AppUser -> Backend : PUT /api/v1/shared/fcm-token\n{fcmToken} +Backend --> AppUser : 200 OK +AppUser -> WS : STOMP connect\n(Authorization: Bearer token) +WS --> AppUser : Connected +AppUser -> User : Redirect ke Home\n(ROLE_USER → /user/home) + +Guardian -> AppGuardian : Login +AppGuardian -> Backend : POST /api/v1/auth/login +Backend --> AppGuardian : 200 OK {accessToken, role=ROLE_GUARDIAN} +AppGuardian -> WS : STOMP connect +WS --> AppGuardian : Connected +AppGuardian -> Guardian : Redirect ke /guardian/home + +' ============================================================ +' 4. AUTH — JWT VALIDATE & REFRESH +' ============================================================ +== 4. JWT Validate & Auto-Refresh == + +AppUser -> Backend : Request API (any endpoint)\n[Authorization: Bearer accessToken] +Backend -> JWT : Validasi token +alt Token valid + JWT --> Backend : OK, userId + role + Backend -> Backend : RBAC check role + Backend --> AppUser : 200 Response +else Token expired (401) + JWT --> Backend : 401 Unauthorized + Backend --> AppUser : 401 Unauthorized + AppUser -> Backend : POST /api/v1/auth/refresh\n{refreshToken} + Backend -> Backend : Cek refresh token di DB\n+ cek expired + alt Refresh token valid + Backend -> Backend : Generate access token baru + Backend --> AppUser : 200 OK {accessToken baru} + AppUser -> AppUser : Update token di SecureStorage + AppUser -> Backend : Retry request original + Backend --> AppUser : 200 Response + else Refresh token expired + Backend --> AppUser : 401 Unauthorized + AppUser -> AppUser : Clear semua token + AppUser -> User : Redirect ke /login + end +end + +' ============================================================ +' 5. PAIRING +' ============================================================ +== 5. Pairing — Guardian Undang User == + +Guardian -> AppGuardian : Input uniqueUserId\n(12 char) milik User +AppGuardian -> Backend : POST /api/v1/guardian/pairing/invite\n{uniqueUserId} +Backend -> Backend : Cek User exist by uniqueUserId +Backend -> Backend : Cek belum ada pairing aktif +Backend -> Backend : Buat PairingRelation\n(status=PENDING) +Backend -> FCM : Kirim push notif ke User\n(type=PAIRING_INVITE) +FCM --> AppUser : Push notification diterima +AppUser -> AppUser : FCM Handler:\nrefresh PairingBloc +AppUser -> AppUser : TTS: "Anda mendapat\nundangan pairing" +Backend --> AppGuardian : 200 OK {pairingId, status=PENDING} + +User -> AppUser : Buka notif / PairingScreen +AppUser -> Backend : GET /api/v1/user/pairing/pending +Backend --> AppUser : 200 OK {pairingId, guardianName} +User -> AppUser : Tap "Terima" atau "Tolak" +AppUser -> Backend : POST /api/v1/user/pairing/respond\n{pairingId, accept: true/false} +Backend -> Backend : Update PairingRelation\n(status=ACTIVE / REJECTED) +alt Accept + Backend -> Backend : Seed default VoiceCommandConfigs\n& HardwareShortcuts untuk pasangan ini + Backend -> FCM : Kirim notif ke Guardian\n(type=PAIRING_RESPONSE, accepted=true) + FCM --> AppGuardian : Push notification + AppGuardian -> AppGuardian : TTS Guardian:\n"User menerima pairing Anda" + Backend --> AppUser : 200 OK {status=ACTIVE} + AppUser -> User : TTS: "Pairing berhasil" +else Reject + Backend -> FCM : Kirim notif ke Guardian\n(type=PAIRING_RESPONSE, accepted=false) + Backend --> AppUser : 200 OK {status=REJECTED} +end + +' ============================================================ +' 6. WALK GUIDE MODE +' ============================================================ +== 6. Aktifkan Walk Guide Mode == + +User -> AppUser : Tap tombol / ucap\n"Start Walkguide" +AppUser -> AppUser : VoiceCommandHandler:\nmatch trigger phrase +AppUser -> AppUser : WalkGuideBloc:\nadd(StartWalkGuide()) +AppUser -> AppUser : Request permission:\nkamera, lokasi, mikrofon +AppUser -> YOLO : loadModel()\n(yolov8n.tflite dari assets) +YOLO --> AppUser : Model loaded +AppUser -> AppUser : Aktifkan kamera stream +AppUser -> AppUser : Aktifkan GPS stream\n(interval 5 detik) +AppUser -> User : TTS: "Walk Guide aktif.\nDeteksi obstacle dimulai." + +loop Setiap frame kamera (max 5 FPS) + AppUser -> YOLO : detect(CameraImage frame) + YOLO -> YOLO : YUV420 → RGB → resize 640×640 + YOLO -> YOLO : Normalize → run inference + YOLO -> YOLO : Post-process: NMS,\nfilter confidence threshold + YOLO --> AppUser : List\n{label, confidence, boundingBox} + alt Ada obstacle terdeteksi + AppUser -> AppUser : ObstacleAnalyzer:\nanalyzeDirection() + estimateDistance() + AppUser -> AppUser : buildTtsMessage():\n"Caution! {label} {direction}. {distance}." + AppUser -> AppUser : ttsService.speakImmediate(message) + AppUser -> User : 🔊 Audio TTS obstacle alert + AppUser -> AppUser : hapticService.obstacle\n{VeryClose/Close/Medium}() + AppUser -> User : 📳 Vibrasi sesuai jarak + AppUser -> Backend : POST /api/v1/user/obstacle-log\n{label, confidence, direction, dist, lat, lng} + Backend --> AppUser : 200 OK + end +end + +' ============================================================ +' 7. KIRIM LOKASI GPS REAL-TIME +' ============================================================ +== 7. Kirim Lokasi GPS Real-time (WebSocket) == + +loop Setiap 5 detik (WalkGuide aktif) / 30 detik (idle) + AppUser -> AppUser : Geolocator:\ngetCurrentPosition() + AppUser -> WS : STOMP send\n/app/location/{userId}\n{lat, lng, accuracy, speed, heading} + WS -> Backend : Proses LocationUpdate + Backend -> Backend : Simpan ke location_history + WS -> AppGuardian : Broadcast ke\n/topic/location/{userId} + AppGuardian -> AppGuardian : Update marker\ndi flutter_map (real-time) + AppGuardian -> Guardian : 🗺️ Peta terupdate + + alt User keluar radius Geofence + Backend -> Backend : Haversine: cek jarak\nke center geofence + Backend -> FCM : Push notif ke Guardian\n(type=GEOFENCE_EXIT) + FCM --> AppGuardian : Notifikasi masuk + AppGuardian -> Guardian : "User telah keluar\ndari area aman!" + end +end + +' ============================================================ +' 8. SOS DARURAT +' ============================================================ +== 8. Kirim SOS Darurat == + +User -> AppUser : Ucap "Send SOS" /\ntap tombol SOS +AppUser -> AppUser : SosBloc: add(TriggerSos()) +AppUser -> AppUser : hapticService.sosTriggered()\n[0,1000,200,1000,200,1000] +AppUser -> AppUser : ttsService.speak:\n"SOS dikirim ke Guardian" +AppUser -> Backend : POST /api/v1/user/sos\n{triggerType, lat, lng} +Backend -> Backend : Simpan SosEvent\n(status=TRIGGERED) +Backend -> Backend : Log ActivityLog: SOS_TRIGGERED +Backend -> FCM : Push notif ke Guardian\n(type=SOS_ALERT, lat, lng) +FCM --> AppGuardian : Push notification SOS +AppGuardian -> AppGuardian : FCM Handler:\nnavigate ke SOS/IncomingCallScreen +AppGuardian -> Guardian : 🚨 Alert SOS dengan lokasi User +Backend --> AppUser : 200 OK {sosEventId, status=TRIGGERED} + +Guardian -> AppGuardian : Tap "Acknowledge" +AppGuardian -> Backend : PUT /api/v1/guardian/sos/{id}/acknowledge +Backend -> Backend : Update status=ACKNOWLEDGED\nsimpan acknowledged_at +Backend --> AppGuardian : 200 OK +AppGuardian -> Guardian : Tampilkan status\n"SOS Diakui" + +Guardian -> AppGuardian : Tap "Resolved" +AppGuardian -> Backend : PUT /api/v1/guardian/sos/{id}/resolve +Backend -> Backend : Update status=RESOLVED +Backend --> AppGuardian : 200 OK + +' ============================================================ +' 9. VoIP CALL (AGORA) +' ============================================================ +== 9. Panggil Guardian (VoIP Agora) == + +User -> AppUser : Ucap "Call Guardian" /\ntap tombol Call +AppUser -> Backend : POST /api/v1/shared/agora/token\n{channelName, uid} +Backend -> Agora : Generate Agora RTC token\n(server-side REST API) +Agora --> Backend : {token, channelName, uid, appId} +Backend --> AppUser : 200 OK {AgoraTokenResponse} +AppUser -> FCM : (via Backend) Push notif ke Guardian\n(type=INCOMING_CALL, channelName) +FCM --> AppGuardian : Push notification panggilan masuk +AppGuardian -> AppGuardian : Navigate ke IncomingCallScreen +AppGuardian -> Guardian : 📞 UI Panggilan Masuk\n(haptic + ringtone) +AppUser -> Agora : joinChannel(token, channelName, uid)\n(audio-only, speakerphone ON) + +Guardian -> AppGuardian : Tap "Angkat" +AppGuardian -> Backend : POST /api/v1/shared/agora/token\n{channelName} +Backend --> AppGuardian : {AgoraTokenResponse} +AppGuardian -> Agora : joinChannel(token, channelName) +Agora --> AppUser : onUserJoined callback +Agora --> AppGuardian : onUserJoined callback +AppUser <-> AppGuardian : 🎙️ Audio call berlangsung\n(via Agora RTC) + +alt Guardian tutup telepon + Guardian -> AppGuardian : Tap "Tutup" + AppGuardian -> Agora : leaveChannel() + Agora --> AppUser : onUserOffline callback + AppUser -> Agora : leaveChannel() + AppUser -> User : TTS: "Panggilan berakhir" +end + +' ============================================================ +' 10. NOTIFIKASI GUARDIAN → USER +' ============================================================ +== 10. Kirim Notifikasi (Teks / Voice Note) == + +Guardian -> AppGuardian : Buka SendNotifScreen\nTulis pesan / rekam voice note +alt Notifikasi Teks + AppGuardian -> Backend : POST /api/v1/guardian/notification\n{notifType=TEXT, content} +else Voice Note + AppGuardian -> AppGuardian : record package:\nrekam audio + AppGuardian -> Backend : POST /api/v1/guardian/notification\n{notifType=VOICE_NOTE,\nvoiceNoteUrl, voiceNoteDuration} +end +Backend -> Backend : Simpan ke guardian_notifications +Backend -> FCM : Kirim push notif ke User\n(type=NOTIFICATION) +FCM --> AppUser : Push notification diterima +AppUser -> AppUser : FCM Handler: type=NOTIFICATION\n→ refresh NotificationBloc +AppUser -> AppUser : flutter_local_notifications:\ntampilkan notif lokal +AppUser -> AppUser : TTS: "Ada pesan baru\ndari Guardian" +AppUser -> User : 🔊 TTS pemberitahuan +Backend --> AppGuardian : 200 OK + +User -> AppUser : Buka NotificationScreen\natau ucap "Read All My Notifications" +AppUser -> Backend : GET /api/v1/user/notifications +Backend --> AppUser : 200 OK List +AppUser -> User : Tampilkan daftar notifikasi +alt Voice Note + User -> AppUser : Tap notifikasi voice note + AppUser -> AppUser : just_audio: play voiceNoteUrl + AppUser -> User : 🔊 Putar voice note Guardian +end +AppUser -> Backend : PUT /api/v1/user/notifications/read-all +Backend -> Backend : Update is_read=true semua notif +Backend --> AppUser : 200 OK + +' ============================================================ +' 11. GUARDIAN DASHBOARD +' ============================================================ +== 11. Guardian Dashboard — Monitor & Konfigurasi == + +Guardian -> AppGuardian : Buka GuardianDashboardScreen +AppGuardian -> Backend : GET /api/v1/guardian/paired-user +Backend --> AppGuardian : 200 OK {UserProfileResponse} +AppGuardian -> Backend : GET /api/v1/guardian/location/latest +Backend --> AppGuardian : 200 OK {lat, lng, speed, heading} +AppGuardian -> AppGuardian : flutter_map: tampilkan\nmarker lokasi User +AppGuardian -> WS : Subscribe /topic/location/{userId} +AppGuardian -> Guardian : 🗺️ Dashboard dengan peta real-time + +Guardian -> AppGuardian : Buka GuardianAiConfigScreen +AppGuardian -> Backend : GET /api/v1/guardian/ai-config +Backend --> AppGuardian : 200 OK {AiConfigResponse} +Guardian -> AppGuardian : Ubah threshold confidence,\ndistance, FPS, enabled labels +AppGuardian -> Backend : PUT /api/v1/guardian/ai-config\n{AiConfigUpdateRequest} +Backend -> Backend : Update ai_configs di DB +Backend -> FCM : Push notif ke User\n(type=SETTINGS_UPDATED) +FCM --> AppUser : Settings update diterima +AppUser -> Backend : GET /api/v1/user/ai-config +Backend --> AppUser : 200 OK {AiConfigResponse terbaru} +AppUser -> YOLO : Update confidenceThreshold\n& maxInferenceFps +Backend --> AppGuardian : 200 OK + +Guardian -> AppGuardian : Buka GuardianGeofenceScreen +AppGuardian -> Backend : GET /api/v1/guardian/geofence +Backend --> AppGuardian : 200 OK {GeofenceResponse} +Guardian -> AppGuardian : Set center (tap peta),\nradius, enable +AppGuardian -> Backend : PUT /api/v1/guardian/geofence\n{centerLat, centerLng, radiusMeters, enabled} +Backend -> Backend : Update geofence_configs +Backend --> AppGuardian : 200 OK +AppGuardian -> Guardian : Tampilkan lingkaran\ngeofence di peta + +Guardian -> AppGuardian : Buka GuardianVoiceCmdScreen +AppGuardian -> Backend : GET /api/v1/guardian/voice-commands +Backend --> AppGuardian : 200 OK List +Guardian -> AppGuardian : Ubah trigger phrase\n(misal: "mulai jalan") +AppGuardian -> Backend : PUT /api/v1/guardian/voice-commands\n{commandKey, triggerPhrase, enabled} +Backend -> Backend : Update voice_command_configs +Backend -> FCM : Push notif ke User\n(type=SETTINGS_UPDATED) +FCM --> AppUser : AppUser reload CachedVoiceCommands\nke SQLite lokal +Backend --> AppGuardian : 200 OK + +Guardian -> AppGuardian : Buka GuardianActivityLogScreen +AppGuardian -> Backend : GET /api/v1/guardian/activity-logs?page=0&size=20 +Backend --> AppGuardian : 200 OK Page +AppGuardian -> Backend : GET /api/v1/guardian/obstacle-logs?page=0&size=20 +Backend --> AppGuardian : 200 OK Page +AppGuardian -> Guardian : Tampilkan riwayat\naktivitas & deteksi obstacle + +' ============================================================ +' 12. LOGOUT +' ============================================================ +== 12. Logout == + +User -> AppUser : Tap Logout +AppUser -> Backend : POST /api/v1/auth/logout +Backend -> Backend : Delete refresh token dari DB +Backend -> Backend : Log ActivityLog: LOGOUT +Backend --> AppUser : 200 OK +AppUser -> WS : disconnect() +AppUser -> AppUser : SecureStorage.clearAll() +AppUser -> AppUser : SharedPreferences: hapus token +AppUser -> User : Redirect ke /login + +@enduml \ No newline at end of file diff --git a/ooad-docs/diagrams/state-machine.puml b/ooad-docs/diagrams/state-machine.puml new file mode 100644 index 0000000..6394644 --- /dev/null +++ b/ooad-docs/diagrams/state-machine.puml @@ -0,0 +1,470 @@ +@startuml WalkGuide_StateMachine +title State Machine Diagram — WalkGuide System (Full Architecture) + +skinparam state { + BackgroundColor WhiteSmoke + BorderColor #5B6AB0 + FontColor Black + ArrowColor #5B6AB0 + StartColor Black + EndColor Black +} +skinparam note { + BackgroundColor LightYellow + BorderColor #999 +} + +' ============================================================ +' TOP LEVEL: APP LAUNCH +' ============================================================ +[*] --> AppLaunched : App dibuka + +state AppLaunched { + [*] --> CheckingServerUrl + + CheckingServerUrl : Cek SharedPreferences\nserver URL tersimpan? + + CheckingServerUrl --> ServerConnectScreen : [Tidak ada URL] + CheckingServerUrl --> CheckingToken : [URL tersedia] + + ServerConnectScreen : Input server URL\n(http://202.46.28.160:8080) + ServerConnectScreen --> PingingServer : Submit URL + + PingingServer : GET /api/v1/auth/ping\n(cek koneksi ke backend) + PingingServer --> ServerConnectScreen : [Ping Gagal / Timeout] + PingingServer --> CheckingToken : [Ping OK → simpan URL ke SharedPreferences] + + CheckingToken : Cek flutter_secure_storage\naccessToken ada? + CheckingToken --> Unauthenticated : [Token Tidak Ada] + CheckingToken --> ValidatingToken : [Token Ada] + + ValidatingToken : Verifikasi JWT\n(decode expiry lokal) + ValidatingToken --> Unauthenticated : [Token Expired / Invalid] + ValidatingToken --> Authenticated : [Token Valid → decode role] +} + +' ============================================================ +' UNAUTHENTICATED +' ============================================================ +state Unauthenticated { + [*] --> LoginScreen + + LoginScreen : Input email & password\n+ tombol ke RegisterScreen + + LoginScreen --> RegisterScreen : Tap "Register" + RegisterScreen --> LoginScreen : Tap "Back to Login" + + RegisterScreen : Pilih role (ROLE_GUARDIAN / ROLE_USER)\nIsi email, password, displayName + + LoginScreen --> ValidatingCredentials : Submit login + RegisterScreen --> RegisteringUser : Submit register + + ValidatingCredentials : POST /api/v1/auth/login\n{email, password} + ValidatingCredentials --> LoginScreen : [401 Unauthorized → tampil error] + ValidatingCredentials --> StoringAuthData : [200 OK + JWT] + + RegisteringUser : POST /api/v1/auth/register\n{email, password, role, displayName} + RegisteringUser --> LoginScreen : [400 / Email sudah terdaftar] + RegisteringUser --> StoringAuthData : [201 Created + JWT] + + StoringAuthData : Simpan accessToken + refreshToken\nke flutter_secure_storage\nUpdate FCM token → PUT /auth/fcm-token\nWebSocketService.connect(accessToken) + StoringAuthData --> DecodingRole : JWT tersimpan + + DecodingRole : Ekstrak role dari JWT claims + DecodingRole --> [*] +} + +note on link + Login Sukses + 200 OK + JWT + (access + refresh) +end note + +Unauthenticated --> Authenticated : [Login / Register Sukses → role decoded] + +' ============================================================ +' AUTHENTICATED (role router) +' ============================================================ +state Authenticated { + [*] --> CheckingPairing + + CheckingPairing : GET /shared/pairing/status\ncek status pairing user ini + + CheckingPairing --> UserShell : [Role = ROLE_USER] + CheckingPairing --> GuardianShell : [Role = ROLE_GUARDIAN] +} + +' ============================================================ +' USER SHELL +' ============================================================ +state UserShell { + + [*] --> UserIdle + + UserIdle : Bottom nav 6 tabs aktif\nSTT always-listening di background\nWebSocket subscribe /queue/notif/{userId} + note right of UserIdle + Voice commands aktif:\n"Start Walkguide", "Open SOS",\n"Call Guardian", "Where Am I", dll + end note + + ' --- PAIRING CHECK --- + UserIdle --> PairingScreen_User : [Belum paired → TTS peringatan] + + state PairingScreen_User { + [*] --> WaitingPairingInvite + WaitingPairingInvite : GET /shared/pairing/status\nTampil uniqueUserId 12-char\n"Share this ID with your Guardian" + WaitingPairingInvite --> ReceivingInvite : FCM: PAIRING_INVITE diterima + ReceivingInvite --> WaitingPairingInvite : Tap "Reject" → POST /pairing/respond {accept:false} + ReceivingInvite --> PairingAccepted : Tap "Accept" → POST /pairing/respond {accept:true} + PairingAccepted : Backend seed 14 VoiceCommands\n+ 5 Shortcuts + AiConfig defaults\nFCM ke Guardian: "Pairing accepted"\nTTS: "Pairing successful. Guardian connected." + PairingAccepted --> [*] + } + + PairingScreen_User --> UserIdle : Pairing berhasil / lewati + + ' --- WALKGUIDE --- + UserIdle --> WalkGuideActive : Tap "Start Walk Guide"\nATAU voice "Start Walkguide"\nATAU Volume Down button + + state WalkGuideActive { + [*] --> InitializingCamera + + InitializingCamera : camera.startImageStream()\nRequest permissions (kamera, lokasi)\nPOST /user/walkguide/start → ActivityLog: WALKGUIDE_START + InitializingCamera --> [*] : [Permission Denied → kembali ke UserIdle] + + InitializingCamera --> DetectionLoop : Kamera siap\nTTS: "3... 2... 1... WalkGuide started." + + state DetectionLoop { + [*] --> CapturingFrame + + CapturingFrame : Terima CameraImage (YUV420)\nThrottle: maxInferenceFps (default 5fps) + CapturingFrame --> ProcessingFrame : [YoloDetector tidak sedang running → kirim frame] + CapturingFrame --> CapturingFrame : [YoloDetector sedang running → skip frame ini] + + ProcessingFrame : YUV420→RGB→Resize 640×640\nNormalize 0.0-1.0\ninterpreter.run() [Isolate terpisah]\nPost-process + NMS\nFilter by confidenceThreshold + + ProcessingFrame --> EvaluatingResult : Inference selesai + + EvaluatingResult : ObstacleAnalyzer.prioritize(results)\nTentukan direction (LEFT/CENTER/RIGHT)\nEstimasi distance (Very Close/Close/Medium/Far) + + EvaluatingResult --> ObstacleDetected : [Confidence ≥ threshold & ada deteksi] + EvaluatingResult --> NoObstacleDetected : [Confidence < threshold / tidak ada deteksi] + + ObstacleDetected : Build TTS: "Caution! {label} {direction}. {distance}. Please stop."\nttsService.speakImmediate()\nhapticService.obstacleVeryClose()\nDebounce 3 detik (jangan log yang sama terus)\nPOST /user/obstacle {label, confidence, direction, dist, lat, lng} + + NoObstacleDetected : Lanjut deteksi + + ObstacleDetected --> CapturingFrame : Feedback selesai + NoObstacleDetected --> CapturingFrame : Lanjut deteksi + + ' Location update loop (parallel, setiap 5 detik) + CapturingFrame --> SendingLocation : [Setiap 5 detik] + SendingLocation : geolocator.getCurrentPosition()\nPOST /user/location {lat, lng, accuracy, speed, heading}\nBackend: simpan LocationHistory\nWebSocket broadcast → Guardian map\nGeofenceService.checkGeofence() + SendingLocation --> CapturingFrame : Location terkirim + } + + DetectionLoop --> SavingSession : Tap "Stop Walk Guide"\nATAU voice "Stop Walkguide" + + SavingSession : camera.stopImageStream()\nPOST /user/walkguide/stop → ActivityLog: WALKGUIDE_STOP\nLocation interval → 30 detik + SavingSession --> [*] + } + + WalkGuideActive --> UserIdle : Sesi WalkGuide selesai + + ' --- SOS --- + UserIdle --> SosFlow : Tap SOS tab\nATAU voice "Send SOS"\nATAU hardware shortcut + + state SosFlow { + [*] --> SosConfirm + SosConfirm : SosScreen: tombol SOS merah besar\nTTS: "SOS screen. Press the big red button\nor say 'Send SOS' to alert your Guardian." + SosConfirm --> TriggeringSos : Tap SOS button / voice "Send SOS" + TriggeringSos : Get GPS position\nPOST /user/sos {triggerType, lat, lng}\nBackend: save SosEvent TRIGGERED\nFCM HIGH PRIORITY ke Guardian: "🚨 SOS ALERT!"\nWebSocket → /queue/sos/{guardianId}\nTTS: "SOS sent. Guardian has been alerted."\nhapticService.sosTriggered() + TriggeringSos --> WaitingAcknowledge : SOS terkirim + WaitingAcknowledge : Tunggu respons Guardian\nTampil status "Waiting for Guardian..." + WaitingAcknowledge --> SosAcknowledged : FCM: Guardian acknowledge SOS\nTTS: "Your Guardian has acknowledged.\nHelp is coming." + SosAcknowledged --> [*] + } + + SosFlow --> UserIdle : Kembali ke home + + ' --- NOTIFICATIONS --- + UserIdle --> NotificationFlow : Tap Notifications tab\nATAU voice "Open Notifications"\nATAU FCM/WebSocket masuk + + state NotificationFlow { + [*] --> ViewingNotifications + ViewingNotifications : GET /user/notifications\nGET /user/notifications/unread-count\nTampil list: TEXT / VOICE_NOTE\nBadge unread count\nTTS: "Notifications. You have {N} unread messages." + ViewingNotifications --> ReadingAllTts : Tap "Read All" / voice "Read All My Notifications" + ViewingNotifications --> ReadingOneTts : Tap satu notifikasi + ReadingAllTts : Iterate unread list:\n- TEXT: TTS "From Guardian on {date}: {content}"\n- VOICE_NOTE: just_audio.play(voiceNoteUrl)\nPUT /user/notifications/{id}/read setelah selesai + ReadingOneTts : TTS / play satu notifikasi\nPUT /user/notifications/{id}/read + ReadingAllTts --> ViewingNotifications : Semua notif dibaca + ReadingOneTts --> ViewingNotifications : Selesai + ViewingNotifications --> MarkingAllRead : Tap "Mark All Read"\nPUT /user/notifications/mark-all-read + MarkingAllRead --> ViewingNotifications : Done + } + + NotificationFlow --> UserIdle : Kembali + + ' --- NAVIGATION MODE --- + UserIdle --> NavigationModeActive : Tap Navigate tab\nATAU voice "Open Navigation" + + state NavigationModeActive { + [*] --> NavigationIdle + NavigationIdle : FlutterMap (OpenStreetMap tiles)\nTTS: "Navigation mode. Say a destination." + NavigationIdle --> SearchingDestination : Tap search bar / voice "Navigate to {place}" + SearchingDestination : OSM Nominatim API\nGET nominatim.openstreetmap.org/search?q=... + SearchingDestination --> NavigatingRoute : Lokasi ditemukan → OSRM routing + NavigatingRoute : OSRM turn-by-turn route\nTTS setiap instruksi: "In 50 meters, turn right"\ngeolocator: GPS real-time\nPOST /user/location setiap 5 detik + NavigatingRoute --> NavigationIdle : Sampai tujuan / Stop + } + + NavigationModeActive --> UserIdle : Kembali + + ' --- CALL --- + UserIdle --> OutgoingCallFlow : Tap "Call Guardian"\nATAU voice "Call Guardian"\nATAU Volume Up button + + state OutgoingCallFlow { + [*] --> RequestingAgoraToken + RequestingAgoraToken : POST /shared/call/token\nGenerate Agora RTC token\nPOST /shared/call/notify → FCM ke Guardian "Incoming Call" + RequestingAgoraToken --> CallingGuardian : Token dapat, join Agora channel + CallingGuardian : CallScreen: "Calling Guardian..." + animasi\nAgoraRtcEngine.joinChannel() + CallingGuardian --> InCall : Guardian pick up → CallConnected + CallingGuardian --> UserIdle : Guardian decline / timeout 30 detik + InCall : Timer durasi call\nTombol: Mute, Speaker, End Call\nTTS: "Connected to Guardian" + InCall --> UserIdle : Tap End Call + } + + UserIdle --> IncomingCallHandled : FCM INCOMING_CALL diterima dari Guardian + + state IncomingCallHandled { + [*] --> ShowingIncomingCall + ShowingIncomingCall : IncomingCallScreen\nTTS: "Incoming call from Guardian"\nHaptic vibration\nTombol Accept (hijau) + Decline (merah)\nAuto-answer setelah 30 detik + ShowingIncomingCall --> InCallSession : Accept + ShowingIncomingCall --> UserIdle : Decline + InCallSession : Agora RTC connected\nTimer + Mute + Speaker + End + InCallSession --> UserIdle : End call + } + + ' --- SETTINGS --- + UserIdle --> UserSettingsFlow : Tap Settings tab\nATAU voice "Open Settings" + + state UserSettingsFlow { + [*] --> ViewingSettings + ViewingSettings : TTS Settings (read-only pitch/speed)\nPairing status link\nManual/Instructions link\nNo-Guardian Warning toggle\nAccount info + Logout + ViewingSettings --> UpdatingTtsLanguage : Toggle ID/EN → PUT /user/settings + ViewingSettings --> ViewingManual : Tap "Instructions" + ViewingSettings --> LoggingOut : Tap Logout + UpdatingTtsLanguage --> ViewingSettings : Saved + ViewingManual : ManualScreen\nSemua voice commands + shortcuts\nTombol "Hear Instructions"\nTTS setiap instruksi 1-per-1 + ViewingManual --> ViewingSettings : Back + LoggingOut : POST /auth/logout {refreshToken}\nSecureStorage.clearAll()\nWebSocketService.disconnect()\nGoRouter → /login + LoggingOut --> [*] + } + + UserSettingsFlow --> UserIdle : Kembali + UserSettingsFlow --> [*] : Logout + + ' --- ACTIVITY LOG --- + UserIdle --> ActivityLogFlow : Tap Activity tab\nATAU voice "Open Activity Log" + + state ActivityLogFlow { + [*] --> ViewingActivityLog + ViewingActivityLog : GET /user/activity-logs (paginated)\nInfinite scroll\nFilter by type/date\nTTS: "Activity log. {N} activities today." + } + + ActivityLogFlow --> UserIdle : Kembali +} + +' ============================================================ +' GUARDIAN SHELL +' ============================================================ +state GuardianShell { + + [*] --> GuardianIdle + + GuardianIdle : GuardianDashboardScreen HOME\nCard: User status (online/offline, battery)\nCard: Last location (Nominatim reverse)\nCard: Unread SOS (badge merah)\nCard: Recent 5 activity logs\nQuick actions: Send Message, Call User, View Map\nWebSocket subscribe:\n /topic/location/{pairedUserId}\n /queue/sos/{guardianId} + + ' --- PAIRING (Guardian side) --- + GuardianIdle --> GuardianPairingFlow : Tap "Manage Pairing"\nATAU belum punya paired user + + state GuardianPairingFlow { + [*] --> EnteringUserId + EnteringUserId : Input uniqueUserId 12-char\nmilik paired User + EnteringUserId --> SendingInvite : Tap "Send Invite" + SendingInvite : POST /shared/pairing/invite {uniqueUserId}\nBackend: buat PairingRelation PENDING\nFCM ke User: "Pairing Request" + SendingInvite --> WaitingUserResponse : Invite terkirim + WaitingUserResponse : Tunggu User accept/reject + WaitingUserResponse --> PairingSuccess : FCM: User accepted\nBackend seed VoiceCommands + Shortcuts + AiConfig + WaitingUserResponse --> EnteringUserId : FCM: User rejected + PairingSuccess : TTS: "Pairing successful. User is now connected."\nNavigate → GuardianIdle + PairingSuccess --> [*] + } + + GuardianPairingFlow --> GuardianIdle : Selesai + + ' --- MAP / LIVE TRACKING --- + GuardianIdle --> GuardianMapView : Tap "View Map"\nATAU quick action map + + state GuardianMapView { + [*] --> ViewingLiveMap + ViewingLiveMap : FlutterMap + live location marker\nWebSocket /topic/location/{userId}\nGeofence circle overlay (jika enabled)\nRoute history polyline (location_history) + ViewingLiveMap --> ViewingLiveMap : [WebSocket update → marker pindah] + } + + GuardianMapView --> GuardianIdle : Kembali + + ' --- ACTIVITY LOGS --- + GuardianIdle --> GuardianActivityLog : Tap "View Activity Logs" + + state GuardianActivityLog { + [*] --> ViewingUserLogs + ViewingUserLogs : GET /guardian/activity-logs (paginated)\nGET /guardian/obstacle-logs\nFilter by type, date + } + + GuardianActivityLog --> GuardianIdle : Kembali + + ' --- SEND NOTIFICATION --- + GuardianIdle --> GuardianSendNotif : Tap "Send Notification" + + state GuardianSendNotif { + [*] --> ComposingNotif + ComposingNotif : Toggle: TEXT / VOICE NOTE + ComposingNotif --> SendingTextNotif : Ketik pesan → Send + ComposingNotif --> RecordingVoiceNote : Tahan tombol Record + SendingTextNotif : POST /guardian/notifications/send\n{type: TEXT, content}\nBackend: FCM ke User + WebSocket\nUser TTS: "New message from Guardian" + RecordingVoiceNote : record package → audio file\nPreview → Upload → POST send\n{type: VOICE_NOTE, voiceNoteUrl} + SendingTextNotif --> ComposingNotif : Sent + RecordingVoiceNote --> ComposingNotif : Sent + } + + GuardianSendNotif --> GuardianIdle : Kembali + + ' --- AI CONFIG --- + GuardianIdle --> GuardianAiConfig : Tap "Configure AI Settings" + + state GuardianAiConfig { + [*] --> EditingAiConfig + EditingAiConfig : GET /guardian/ai-config\nSlider confidenceThreshold (0.3-0.9)\nSlider alertDistanceClose (0.5-3m)\nSlider alertDistanceMedium (1-6m)\nDropdown maxInferenceFps (1,2,3,5,10)\nMulti-select enabled labels + EditingAiConfig --> SavingAiConfig : Tap Save + SavingAiConfig : PUT /guardian/ai-config\nBackend: update + FCM ke User\n"Guardian updated AI settings" + SavingAiConfig --> EditingAiConfig : Saved + } + + GuardianAiConfig --> GuardianIdle : Kembali + + ' --- VOICE COMMANDS CONFIG --- + GuardianIdle --> GuardianVoiceCmdConfig : Tap "Voice Commands" + + state GuardianVoiceCmdConfig { + [*] --> EditingVoiceCommands + EditingVoiceCommands : GET /guardian/voice-commands\nList 14 commands\nEdit trigger phrase\nToggle enabled/disabled + EditingVoiceCommands --> SavingVoiceCmd : Save + SavingVoiceCmd : PUT /guardian/voice-commands\nFCM ke User: "Voice commands updated" + SavingVoiceCmd --> EditingVoiceCommands : Saved + } + + GuardianVoiceCmdConfig --> GuardianIdle : Kembali + + ' --- HARDWARE SHORTCUTS --- + GuardianIdle --> GuardianShortcutConfig : Tap "Shortcuts" + + state GuardianShortcutConfig { + [*] --> EditingShortcuts + EditingShortcuts : GET /guardian/shortcuts\nList 5 shortcuts (Vol Up, Vol Down, dll)\n"Unassigned" jika belum di-set + EditingShortcuts --> CapturingButton : Tap "Configure" per shortcut + CapturingButton : FCM ke User's phone "enter capture mode"\nUser tekan tombol fisik → keyCode\nPUT /user/shortcuts {shortcutKey, buttonCode} + CapturingButton --> EditingShortcuts : Button captured & saved + } + + GuardianShortcutConfig --> GuardianIdle : Kembali + + ' --- GEOFENCE --- + GuardianIdle --> GuardianGeofenceConfig : Tap "Geofence" + + state GuardianGeofenceConfig { + [*] --> EditingGeofence + EditingGeofence : FlutterMap: tap untuk set center\nSlider radius (50m - 5000m)\nToggle enable/disable geofence + EditingGeofence --> SavingGeofence : Tap Save + SavingGeofence : PUT /guardian/geofence\nBackend: simpan GeofenceConfig + SavingGeofence --> EditingGeofence : Saved + } + + GuardianGeofenceConfig --> GuardianIdle : Kembali + + ' --- SOS HANDLING --- + GuardianIdle --> SosAlertReceived : FCM HIGH PRIORITY / WebSocket SOS alert\n/queue/sos/{guardianId} + + state SosAlertReceived { + [*] --> DisplayingSosAlert + DisplayingSosAlert : GuardianDashboard: highlight merah\nMap otomatis buka ke lokasi User\nTampil: "🚨 {User} needs help! Location: lat,lng"\nGET /guardian/sos-events + DisplayingSosAlert --> AcknowledgingSos : Tap "Acknowledge" + AcknowledgingSos : PUT /guardian/sos/{id}/acknowledge\nBackend: status → ACKNOWLEDGED\nFCM ke User: "Guardian is on the way"\nUser TTS: "Help is coming." + AcknowledgingSos --> [*] + } + + SosAlertReceived --> GuardianIdle : Selesai + + ' --- CALL (Guardian inisiasi) --- + GuardianIdle --> GuardianOutgoingCall : Tap "Call User" + + state GuardianOutgoingCall { + [*] --> GuardianRequestToken + GuardianRequestToken : POST /shared/call/token\nPOST /shared/call/notify → FCM ke User "Incoming Call" + GuardianRequestToken --> GuardianCalling : Join Agora channel + GuardianCalling : "Calling User..." + ring animasi + GuardianCalling --> GuardianInCall : User accept + GuardianCalling --> GuardianIdle : User decline / timeout + GuardianInCall : Audio call aktif\nMute + Speaker + End + GuardianInCall --> GuardianIdle : End call + } + + GuardianOutgoingCall --> GuardianIdle : Selesai + + ' --- INCOMING CALL (Guardian terima) --- + GuardianIdle --> GuardianIncomingCall : FCM INCOMING_CALL dari User + + state GuardianIncomingCall { + [*] --> GuardianShowIncomingCall + GuardianShowIncomingCall : IncomingCallScreen\nTTS: "Incoming call from User"\nAccept (hijau) / Decline (merah) + GuardianShowIncomingCall --> GuardianInCallSession : Accept + GuardianShowIncomingCall --> GuardianIdle : Decline + GuardianInCallSession : Agora RTC connected + GuardianInCallSession --> GuardianIdle : End call + } + + GuardianIncomingCall --> GuardianIdle : Selesai + + ' --- LOGOUT --- + GuardianIdle --> GuardianLoggingOut : Tap Logout + + GuardianLoggingOut : POST /auth/logout {refreshToken}\nSecureStorage.clearAll()\nWebSocket.disconnect() + GuardianLoggingOut --> [*] +} + +' ============================================================ +' GLOBAL LOGOUT / TOKEN REFRESH +' ============================================================ +UserShell --> Unauthenticated : Logout / Token Expired (401 → refresh gagal) +GuardianShell --> Unauthenticated : Logout / Token Expired (401 → refresh gagal) + +note as TokenRefreshNote + **Token Refresh (AuthInterceptor):** + Setiap request: inject "Authorization: Bearer {accessToken}" + Jika 401 → POST /auth/refresh {refreshToken} + Jika refresh OK → retry request dengan token baru + Jika refresh 401 → navigate ke /login + Access token: 1 jam | Refresh token: 30 hari +end note + +' ============================================================ +' GEOFENCE EXIT (Parallel event) +' ============================================================ +note as GeofenceNote + **Geofence Alert (Backend event):** + Setiap POST /user/location (saat WalkGuide aktif, 5 detik): + Backend: GeofenceService.checkGeofence() + → Haversine distance dari center ke current position + → Jika keluar radius: FCM ke Guardian "User has left geofence!" + → ActivityLog: GEOFENCE_EXIT + → Guardian: notifikasi di GuardianDashboard +end note + +@enduml diff --git a/ooad-docs/diagrams/usecase-diagram.puml b/ooad-docs/diagrams/usecase-diagram.puml new file mode 100644 index 0000000..0613ce1 --- /dev/null +++ b/ooad-docs/diagrams/usecase-diagram.puml @@ -0,0 +1,176 @@ +@startuml +left to right direction +skinparam packageStyle rectangle +skinparam rectangle { + BackgroundColor<> LightGray + BorderColor Black +} + +' ========================================== +' AKTOR UTAMA (Kiri) +' ========================================== +actor "Tunanetra\n(ROLE_USER)" as User +actor "Pendamping\n(ROLE_GUARDIAN)" as Guardian + +' ========================================== +' BATAS SISTEM (Tengah) +' ========================================== +rectangle "WalkGuide System" { + + ' --- AUTH --- + usecase "Hubungkan ke Server" as UC_Server + usecase "Daftar Akun" as UC_Register + usecase "Masuk Sistem (Login)" as UC_Login + usecase "Logout" as UC_Logout + usecase "Validasi & Refresh JWT" as UC_JWT + usecase "RBAC Role Routing" as UC_RBAC + + ' --- PAIRING --- + usecase "Kirim Undangan Pairing" as UC_Invite + usecase "Terima / Tolak Pairing" as UC_PairingResponse + usecase "Putus Pairing (Unpair)" as UC_Unpair + + ' --- WALKGUIDE CORE --- + usecase "Aktifkan Walk Guide Mode" as UC_Walk + usecase "Deteksi Obstacle\nReal-time (YOLO)" as UC_Detect + usecase "Analisa Arah & Jarak\nObstacle" as UC_Analyze + usecase "Umpan Balik TTS (Audio)" as UC_TTS + usecase "Umpan Balik Vibrasi\n(Haptic)" as UC_Vib + usecase "Log Obstacle ke Backend" as UC_ObsLog + + ' --- LOKASI --- + usecase "Kirim Lokasi GPS\nReal-time (WebSocket)" as UC_Location + usecase "Mode Navigasi\n(OpenStreetMap)" as UC_Navigation + usecase "\"Where Am I?\"\n(Reverse Geocode)" as UC_WhereAmI + + ' --- VOICE & SHORTCUT --- + usecase "Perintah Suara\n(Speech-to-Text)" as UC_Voice + usecase "Shortcut Tombol\nFisik Hardware" as UC_Hardware + + ' --- SOS & DARURAT --- + usecase "Kirim SOS (Darurat)" as UC_SOS + usecase "Panggil Guardian\n(VoIP Agora)" as UC_Call + usecase "Terima Panggilan Masuk" as UC_IncomingCall + + ' --- NOTIFIKASI --- + usecase "Terima Notifikasi FCM" as UC_FCMReceive + usecase "Kirim Notifikasi\nTeks / Voice Note" as UC_SendNotif + usecase "Baca Notifikasi via TTS" as UC_ReadNotif + usecase "Tandai Notifikasi\nSudah Dibaca" as UC_MarkRead + + ' --- LOG --- + usecase "Lihat Log Aktivitas" as UC_ViewLog + usecase "Lihat Log Obstacle" as UC_ViewObsLog + + ' --- PENGATURAN --- + usecase "Atur Preferensi TTS\n& Haptic" as UC_UserSettings + usecase "Kelola Perintah\nSuara Kustom" as UC_VoiceConfig + usecase "Kelola Shortcut\nTombol Fisik" as UC_ShortcutConfig + + ' --- GUARDIAN DASHBOARD --- + usecase "Monitor Lokasi User\nReal-time (Map)" as UC_MonitorMap + usecase "Kelola Konfigurasi AI\n(Threshold YOLO)" as UC_AIConfig + usecase "Kelola Geofence\n(Radius Aman)" as UC_Geofence + usecase "Kelola Akun\nUser Paired" as UC_ManageUser + usecase "Konfirmasi/Acknowledge\nSOS" as UC_AckSOS +} + +' ========================================== +' AKTOR SISTEM (Kanan) +' ========================================== +rectangle "YOLOv8n\n(On-Device AI)" as AI <> +rectangle "Spring Boot\nBackend" as Backend <> +rectangle "Firebase FCM" as FCM <> +rectangle "Agora RTC" as Agora <> + +' ========================================== +' RELASI USER (ROLE_USER) +' ========================================== +User "*" -- "*" UC_Server +User "*" -- "*" UC_Login +User "*" -- "*" UC_Register +User "*" -- "*" UC_Logout +User "*" -- "*" UC_PairingResponse +User "*" -- "*" UC_Unpair +User "*" -- "*" UC_Walk +User "*" -- "*" UC_Voice +User "*" -- "*" UC_Hardware +User "*" -- "*" UC_SOS +User "*" -- "*" UC_Call +User "*" -- "*" UC_IncomingCall +User "*" -- "*" UC_Navigation +User "*" -- "*" UC_WhereAmI +User "*" -- "*" UC_FCMReceive +User "*" -- "*" UC_ReadNotif +User "*" -- "*" UC_MarkRead +User "*" -- "*" UC_ViewLog +User "*" -- "*" UC_UserSettings +User "*" -- "*" UC_ShortcutConfig + +' ========================================== +' RELASI GUARDIAN (ROLE_GUARDIAN) +' ========================================== +Guardian "*" -- "*" UC_Login +Guardian "*" -- "*" UC_Register +Guardian "*" -- "*" UC_Logout +Guardian "*" -- "*" UC_Invite +Guardian "*" -- "*" UC_Unpair +Guardian "*" -- "*" UC_MonitorMap +Guardian "*" -- "*" UC_SendNotif +Guardian "*" -- "*" UC_AIConfig +Guardian "*" -- "*" UC_Geofence +Guardian "*" -- "*" UC_VoiceConfig +Guardian "*" -- "*" UC_ShortcutConfig +Guardian "*" -- "*" UC_ManageUser +Guardian "*" -- "*" UC_IncomingCall +Guardian "*" -- "*" UC_ViewLog +Guardian "*" -- "*" UC_ViewObsLog +Guardian "*" -- "*" UC_Call +Guardian "*" -- "*" UC_AckSOS + +' ========================================== +' INCLUDE & EXTEND +' ========================================== +UC_Login ..> UC_JWT : <> +UC_Register ..> UC_JWT : <> +UC_JWT ..> UC_RBAC : <> + +UC_Walk ..> UC_Detect : <> +UC_Detect ..> UC_Analyze : <> +UC_Analyze <.. UC_TTS : <> +UC_Analyze <.. UC_Vib : <> +UC_Detect ..> UC_ObsLog : <> +UC_Walk ..> UC_Location : <> + +UC_Walk <.. UC_Voice : <> +UC_SOS <.. UC_Voice : <> +UC_Call <.. UC_Voice : <> + +UC_Walk <.. UC_Hardware : <> +UC_SOS <.. UC_Hardware : <> +UC_Call <.. UC_Hardware : <> + +UC_SOS ..> UC_FCMReceive : <> +UC_SendNotif ..> UC_FCMReceive : <> +UC_FCMReceive <.. UC_TTS : <> +UC_Call <.. UC_IncomingCall : <> +UC_Geofence <.. UC_FCMReceive : <> + +' ========================================== +' RELASI KE SISTEM EKSTERNAL +' ========================================== +UC_Detect "*" ----- "*" AI +UC_JWT "*" ----- "*" Backend +UC_ObsLog "*" ----- "*" Backend +UC_Location "*" ----- "*" Backend +UC_SOS "*" ----- "*" Backend +UC_SendNotif "*" ----- "*" Backend +UC_ViewLog "*" ----- "*" Backend +UC_ViewObsLog "*" ----- "*" Backend +UC_AIConfig "*" ----- "*" Backend +UC_FCMReceive "*" ----- "*" FCM +UC_SendNotif "*" ----- "*" FCM +UC_Call "*" ----- "*" Agora +UC_IncomingCall "*" ----- "*" Agora + +@enduml \ No newline at end of file