471 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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