docs: finalize UML and ER diagrams for WalkGuide architecture

This commit is contained in:
5803024019 2026-05-19 16:41:21 +07:00
parent 20e458e406
commit 2cbd6bda3a
6 changed files with 1562 additions and 28 deletions

View File

@ -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 V1V16):
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

View File

@ -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<DetectionResult>\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<NotificationResponse>
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<VoiceCommandResponse>
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<ActivityLogResponse>
AppGuardian -> Backend : GET /api/v1/guardian/obstacle-logs?page=0&size=20
Backend --> AppGuardian : 200 OK Page<ObstacleLogResponse>
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

View File

@ -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

View File

@ -0,0 +1,176 @@
@startuml
left to right direction
skinparam packageStyle rectangle
skinparam rectangle {
BackgroundColor<<actor>> 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 <<actor>>
rectangle "Spring Boot\nBackend" as Backend <<actor>>
rectangle "Firebase FCM" as FCM <<actor>>
rectangle "Agora RTC" as Agora <<actor>>
' ==========================================
' 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 : <<include>>
UC_Register ..> UC_JWT : <<include>>
UC_JWT ..> UC_RBAC : <<include>>
UC_Walk ..> UC_Detect : <<include>>
UC_Detect ..> UC_Analyze : <<include>>
UC_Analyze <.. UC_TTS : <<extend>>
UC_Analyze <.. UC_Vib : <<extend>>
UC_Detect ..> UC_ObsLog : <<include>>
UC_Walk ..> UC_Location : <<include>>
UC_Walk <.. UC_Voice : <<extend>>
UC_SOS <.. UC_Voice : <<extend>>
UC_Call <.. UC_Voice : <<extend>>
UC_Walk <.. UC_Hardware : <<extend>>
UC_SOS <.. UC_Hardware : <<extend>>
UC_Call <.. UC_Hardware : <<extend>>
UC_SOS ..> UC_FCMReceive : <<include>>
UC_SendNotif ..> UC_FCMReceive : <<include>>
UC_FCMReceive <.. UC_TTS : <<extend>>
UC_Call <.. UC_IncomingCall : <<extend>>
UC_Geofence <.. UC_FCMReceive : <<extend>>
' ==========================================
' 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