2026-06-03 14:25:20 +02:00
|
|
|
package com.hawhamburg.micr0bu.viewmodel
|
|
|
|
|
|
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
|
|
|
import androidx.lifecycle.viewModelScope
|
2026-07-02 15:04:34 +02:00
|
|
|
import com.hawhamburg.micr0bu.data.cam.CamUseCaseRepository
|
2026-06-03 14:25:20 +02:00
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
|
|
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.MqttMessage
|
|
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.MqttPreferences
|
|
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.MqttPrefs
|
|
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.MqttRepository
|
2026-08-04 17:58:07 +02:00
|
|
|
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
|
|
|
|
|
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
2026-06-03 14:25:20 +02:00
|
|
|
import com.hawhamburg.micr0bu.data.transport.TransportType
|
|
|
|
|
import com.hawhamburg.micr0bu.data.transport.UsbNetworkDetector
|
2026-07-02 15:04:34 +02:00
|
|
|
import com.hawhamburg.micr0bu.domain.denm.DenmUseCase
|
|
|
|
|
import com.hawhamburg.micr0bu.domain.usecase.UseCaseAlert
|
|
|
|
|
import com.hawhamburg.micr0bu.domain.usecase.UseCaseType
|
2026-06-03 14:25:20 +02:00
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
@HiltViewModel
|
|
|
|
|
class MqttViewModel @Inject constructor(
|
|
|
|
|
private val repo: MqttRepository,
|
|
|
|
|
private val prefs: MqttPreferences,
|
|
|
|
|
private val usbDetector: UsbNetworkDetector,
|
2026-07-02 15:04:34 +02:00
|
|
|
private val camUseCaseRepository: CamUseCaseRepository,
|
2026-08-04 17:58:07 +02:00
|
|
|
private val obuHardwarePrefs: ObuHardwarePreferences,
|
2026-06-03 14:25:20 +02:00
|
|
|
) : ViewModel() {
|
|
|
|
|
|
|
|
|
|
// ── MQTT connection & messages ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
val connectionState: StateFlow<MqttConnectionState> = repo.connectionState
|
|
|
|
|
|
2026-07-02 15:04:34 +02:00
|
|
|
val topicMessages: StateFlow<Map<String, List<MqttMessage>>> = repo.topicMessages
|
2026-06-03 14:25:20 +02:00
|
|
|
|
|
|
|
|
private val _selectedTopic = MutableStateFlow<String?>(null)
|
|
|
|
|
val selectedTopic: StateFlow<String?> = _selectedTopic.asStateFlow()
|
|
|
|
|
|
|
|
|
|
private val _autoScroll = MutableStateFlow(true)
|
|
|
|
|
val autoScroll: StateFlow<Boolean> = _autoScroll.asStateFlow()
|
|
|
|
|
|
|
|
|
|
// ── Transport & USB ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
val activeTransport: StateFlow<TransportType> = repo.activeTransport
|
|
|
|
|
|
2026-08-04 17:58:07 +02:00
|
|
|
/** Which physical OBU (Section 13) is currently selected — CiT One or ESP32-C5. */
|
|
|
|
|
val obuHardware: StateFlow<ObuHardware> = repo.obuHardware
|
|
|
|
|
|
|
|
|
|
fun setObuHardware(hardware: ObuHardware) {
|
|
|
|
|
viewModelScope.launch { obuHardwarePrefs.setObuHardware(hardware) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 14:25:20 +02:00
|
|
|
/** True when a 192.168.42.x USB-C tethering network is detected. */
|
|
|
|
|
val usbConnected: StateFlow<Boolean> = usbDetector.usbNetwork
|
|
|
|
|
.map { it != null }
|
|
|
|
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
|
|
|
|
|
|
|
|
|
/** Auto-detected OBU gateway IP on the USB interface. */
|
|
|
|
|
val detectedObuIp: StateFlow<String?> = usbDetector.detectedGatewayIp
|
|
|
|
|
|
|
|
|
|
// ── Prefs ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
val mqttPrefs: StateFlow<MqttPrefs> = prefs.prefsFlow.stateIn(
|
|
|
|
|
viewModelScope,
|
|
|
|
|
SharingStarted.Eagerly,
|
|
|
|
|
MqttPrefs(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── OBU identity (parsed from v2x/rx/obu_gnss own_info) ──────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* stationType from the OBU's own_info (v2x/rx/obu_gnss). Should be 2 (cyclist/VRU) per
|
|
|
|
|
* ETSI EN 302 637-2 Table 1. Null until the first obu_gnss message arrives.
|
|
|
|
|
*/
|
2026-07-02 15:04:34 +02:00
|
|
|
private val _obuStationType = MutableStateFlow<Int?>(null)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The raw stationType value last reported by the OBU. Null until the first
|
|
|
|
|
* obu_gnss message arrives. Exposed so the UI can show the actual value.
|
|
|
|
|
*/
|
2026-06-03 14:25:20 +02:00
|
|
|
val obuStationType: StateFlow<Int?> = _obuStationType.asStateFlow()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* True when the OBU has reported a stationType other than 2 (cyclist).
|
|
|
|
|
* Triggers a persistent warning banner — an incorrect stationType means this OBU will
|
|
|
|
|
* not be detected as a VRU at equipped intersections.
|
|
|
|
|
*/
|
|
|
|
|
val obuStationTypeWarning: StateFlow<Boolean> = _obuStationType
|
|
|
|
|
.map { it != null && it != 2 }
|
|
|
|
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
|
|
|
|
|
|
|
|
|
// ── DENM transmission ─────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-02 15:04:34 +02:00
|
|
|
/** True while a DENM use case is actively broadcasting on the OBU. */
|
|
|
|
|
val denmActive: StateFlow<Boolean> = repo.denmActive
|
2026-06-03 14:25:20 +02:00
|
|
|
|
|
|
|
|
/** JSON string of the most recently transmitted DENM control message. */
|
2026-07-02 15:04:34 +02:00
|
|
|
val lastDenmPayload: StateFlow<String?> = repo.lastDenmPayload
|
2026-06-03 14:25:20 +02:00
|
|
|
|
2026-07-02 15:04:34 +02:00
|
|
|
/** The use case id currently active on the OBU, if any (e.g. for showing in the UI). */
|
|
|
|
|
val activeDenmUseCase: StateFlow<String?> = repo.activeDenmUseCase
|
|
|
|
|
|
|
|
|
|
// ── CAM-based Use Case Detection (Section 10.2 / 10.4) ────────────────────
|
|
|
|
|
// Entirely separate from the DENM transmission above: this consumes CAM only, raises
|
|
|
|
|
// local HMI alerts, and never triggers an outbound V2X message.
|
|
|
|
|
|
|
|
|
|
/** The ego OBU's own station ID, learned from v2x/rx/obu_gnss's own_info. */
|
|
|
|
|
val ownStationId: StateFlow<Long?> = camUseCaseRepository.ownStationId
|
|
|
|
|
|
|
|
|
|
/** Active CAM-based use case alerts, filtered to the use cases enabled in Settings. */
|
|
|
|
|
val useCaseAlerts: StateFlow<List<UseCaseAlert>> = camUseCaseRepository.enabledAlerts
|
|
|
|
|
|
|
|
|
|
/** Per-use-case enable/disable state (Settings > Use Case Alerts). */
|
|
|
|
|
val useCaseEnabledMap: StateFlow<Map<UseCaseType, Boolean>> = camUseCaseRepository.enabledMap
|
|
|
|
|
|
2026-08-04 17:58:07 +02:00
|
|
|
/** Ego bike's latest known position, for the V2X Monitor live map view (Section 13). */
|
|
|
|
|
val ownCamPosition: StateFlow<com.hawhamburg.micr0bu.domain.cam.Cam?> = camUseCaseRepository.ownPosition
|
|
|
|
|
|
|
|
|
|
/** Latest known CAM per tracked remote road user, for the live map view (Section 13). */
|
|
|
|
|
val remoteCamPositions: StateFlow<Map<Long, com.hawhamburg.micr0bu.domain.cam.Cam>> = camUseCaseRepository.remotePositions
|
|
|
|
|
|
2026-07-02 15:04:34 +02:00
|
|
|
/** True if [stationId] is the ego OBU's own — used for OWN/REMOTE badges in the raw message list. */
|
|
|
|
|
fun isOwnStationId(stationId: Long): Boolean = camUseCaseRepository.isOwnStationId(stationId)
|
|
|
|
|
|
|
|
|
|
fun setUseCaseEnabled(type: UseCaseType, enabled: Boolean) {
|
|
|
|
|
camUseCaseRepository.setUseCaseEnabled(type, enabled)
|
|
|
|
|
}
|
2026-06-03 14:25:20 +02:00
|
|
|
|
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
viewModelScope.launch {
|
2026-07-02 15:04:34 +02:00
|
|
|
// Per-topic message lists now live in MqttRepository (survives screen close);
|
|
|
|
|
// here we just watch for own_info to track the OBU's reported stationType.
|
2026-06-03 14:25:20 +02:00
|
|
|
repo.messages.collect { msg ->
|
|
|
|
|
if (msg.topic == "v2x/rx/obu_gnss") {
|
|
|
|
|
runCatching {
|
|
|
|
|
val stType = JSONObject(msg.payload)
|
|
|
|
|
.optJSONObject("own_info")
|
|
|
|
|
?.optInt("stationType", -1) ?: -1
|
|
|
|
|
if (stType >= 0) _obuStationType.value = stType
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
fun connect() = repo.connect()
|
|
|
|
|
fun disconnect() = repo.disconnect()
|
|
|
|
|
|
|
|
|
|
fun selectTopic(topic: String?) { _selectedTopic.value = topic }
|
|
|
|
|
fun setAutoScroll(enabled: Boolean) { _autoScroll.value = enabled }
|
|
|
|
|
|
|
|
|
|
fun updatePrefs(newPrefs: MqttPrefs) {
|
|
|
|
|
viewModelScope.launch { prefs.update(newPrefs) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── DENM actions ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Publish a uca-denmctrl activate message with the retain flag so the OBU's Use Case app
|
2026-07-02 15:04:34 +02:00
|
|
|
* receives it on any (re)connect. Only one use case may be active at a time
|
|
|
|
|
* (no-op if another use case, manual or automatic, is already active).
|
2026-06-03 14:25:20 +02:00
|
|
|
*/
|
2026-07-02 15:04:34 +02:00
|
|
|
fun sendDenm(useCase: String = DenmUseCase.STATIONARY.id) {
|
|
|
|
|
repo.activateDenm(useCase)
|
2026-06-03 14:25:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-02 15:04:34 +02:00
|
|
|
* Publish a uca-denmctrl deactivate message (retained) for whichever use case is
|
|
|
|
|
* currently active.
|
2026-06-03 14:25:20 +02:00
|
|
|
*/
|
|
|
|
|
fun stopDenm() {
|
2026-07-02 15:04:34 +02:00
|
|
|
repo.deactivateDenm()
|
2026-06-03 14:25:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
override fun onCleared() {
|
|
|
|
|
super.onCleared()
|
|
|
|
|
repo.disconnect()
|
|
|
|
|
}
|
|
|
|
|
}
|