381 lines
17 KiB
Plaintext
381 lines
17 KiB
Plaintext
@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 |