@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