sforkowany z mirror/meshtastic-android
feat: add remote node configuration (#626)
rodzic
ec3a046fb6
commit
85e62eaab4
|
@ -58,6 +58,9 @@ interface IMeshService {
|
|||
*/
|
||||
void setOwner(in MeshUser user);
|
||||
|
||||
void setRemoteOwner(in int destNum, in byte []payload);
|
||||
void getRemoteOwner(in int requestId, in int destNum);
|
||||
|
||||
/// Return my unique user ID string
|
||||
String getMyId();
|
||||
|
||||
|
@ -87,9 +90,21 @@ interface IMeshService {
|
|||
/// It sets a Config protobuf via admin packet
|
||||
void setConfig(in byte []payload);
|
||||
|
||||
/// This method is only intended for use in our GUI, so the user can set radio options
|
||||
/// It sets a ModuleConfig protobuf via admin packet
|
||||
void setModuleConfig(in byte []payload);
|
||||
/// Set and get a Config protobuf via admin packet
|
||||
void setRemoteConfig(in int destNum, in byte []payload);
|
||||
void getRemoteConfig(in int requestId, in int destNum, in int configTypeValue);
|
||||
|
||||
/// Set and get a ModuleConfig protobuf via admin packet
|
||||
void setModuleConfig(in int destNum, in byte []payload);
|
||||
void getModuleConfig(in int requestId, in int destNum, in int moduleConfigTypeValue);
|
||||
|
||||
/// Set and get the Ext Notification Ringtone string via admin packet
|
||||
void setRingtone(in int destNum, in String ringtone);
|
||||
void getRingtone(in int requestId, in int destNum);
|
||||
|
||||
/// Set and get the Canned Message Messages string via admin packet
|
||||
void setCannedMessages(in int destNum, in String messages);
|
||||
void getCannedMessages(in int requestId, in int destNum);
|
||||
|
||||
/// This method is only intended for use in our GUI, so the user can set radio options
|
||||
/// It sets a Channel protobuf via admin packet
|
||||
|
@ -102,22 +117,22 @@ interface IMeshService {
|
|||
void commitEditSettings();
|
||||
|
||||
/// Send position packet with wantResponse to nodeNum
|
||||
void requestPosition(in int idNum, in Position position);
|
||||
void requestPosition(in int destNum, in Position position);
|
||||
|
||||
/// Send traceroute packet with wantResponse to nodeNum
|
||||
void requestTraceroute(in int requestId, in int destNum);
|
||||
|
||||
/// Send Shutdown admin packet to nodeNum
|
||||
void requestShutdown(in int idNum);
|
||||
void requestShutdown(in int destNum);
|
||||
|
||||
/// Send Reboot admin packet to nodeNum
|
||||
void requestReboot(in int idNum);
|
||||
void requestReboot(in int destNum);
|
||||
|
||||
/// Send FactoryReset admin packet to nodeNum
|
||||
void requestFactoryReset(in int idNum);
|
||||
void requestFactoryReset(in int destNum);
|
||||
|
||||
/// Send NodedbReset admin packet to nodeNum
|
||||
void requestNodedbReset(in int idNum);
|
||||
void requestNodedbReset(in int destNum);
|
||||
|
||||
/// Returns a ChannelSet protobuf
|
||||
byte []getChannelSet();
|
||||
|
|
|
@ -769,8 +769,9 @@ class MainActivity : AppCompatActivity(), Logging {
|
|||
return true
|
||||
}
|
||||
R.id.radio_config -> {
|
||||
val node = model.ourNodeInfo.value ?: return true
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.mainActivityLayout, DeviceSettingsFragment())
|
||||
.add(R.id.mainActivityLayout, DeviceSettingsFragment(node))
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
return true
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.geeksville.mesh.database.entity.MeshLog
|
|||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos.User
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.repository.datastore.ChannelSetRepository
|
||||
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
|
||||
|
@ -34,7 +35,6 @@ import com.geeksville.mesh.util.positionToMeter
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.mapLatest
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.osmdroid.bonuspack.kml.KmlDocument
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.FolderOverlay
|
||||
|
@ -120,6 +119,10 @@ class UIViewModel @Inject constructor(
|
|||
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
|
||||
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
|
||||
|
||||
private val requestId = MutableStateFlow<Int?>(null)
|
||||
private val _packetResponse = MutableStateFlow<MeshLog?>(null)
|
||||
val packetResponse: StateFlow<MeshLog?> = _packetResponse
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
meshLogRepository.getAllLogs().collect { logs ->
|
||||
|
@ -148,6 +151,13 @@ class UIViewModel @Inject constructor(
|
|||
combine(nodeDB.nodes.asFlow(), nodeDB.myId.asFlow()) { nodes, id -> nodes[id] }.onEach {
|
||||
_ourNodeInfo.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
combine(_meshLog, requestId) { packet, requestId ->
|
||||
requestId?.run { packet.lastOrNull { it.meshPacket?.decoded?.requestId == requestId } }
|
||||
}.onEach {
|
||||
_packetResponse.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
debug("ViewModel created")
|
||||
}
|
||||
|
||||
|
@ -174,42 +184,12 @@ class UIViewModel @Inject constructor(
|
|||
.filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 }
|
||||
}.asLiveData()
|
||||
|
||||
private val _packetResponse = MutableStateFlow<MeshLog?>(null)
|
||||
val packetResponse: StateFlow<MeshLog?> = _packetResponse
|
||||
|
||||
/**
|
||||
* Called immediately after activity observes packetResponse
|
||||
*/
|
||||
fun clearPacketResponse() {
|
||||
_packetResponse.tryEmit(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet response to a given [packetId] or null after [timeout] milliseconds
|
||||
*/
|
||||
private suspend fun getResponseBy(packetId: Int, timeout: Long) = withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(timeout) {
|
||||
var packet: MeshLog? = null
|
||||
while (packet == null) {
|
||||
packet = _meshLog.value.lastOrNull { it.meshPacket?.decoded?.requestId == packetId }
|
||||
if (packet == null) delay(1000)
|
||||
}
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTraceroute(destNum: Int) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
try {
|
||||
val packetId = service.packetId
|
||||
val waitFactor = (service.nodes.count { it.isOnline } - 1)
|
||||
.coerceAtMost(config.lora.hopLimit)
|
||||
service.requestTraceroute(packetId, destNum)
|
||||
_packetResponse.emit(getResponseBy(packetId, 20000L * waitFactor))
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Request traceroute error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
requestId.value = null
|
||||
_packetResponse.value = null
|
||||
}
|
||||
|
||||
fun generatePacketId(): Int? {
|
||||
|
@ -247,6 +227,69 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun request(
|
||||
destNum: Int,
|
||||
requestAction: suspend (IMeshService, Int, Int) -> Unit,
|
||||
errorMessage: String,
|
||||
configType: Int = 0
|
||||
) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
requestId.value = packetId
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("$errorMessage: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOwner(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
|
||||
"Request getOwner error"
|
||||
)
|
||||
|
||||
fun getConfig(destNum: Int, configType: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteConfig(packetId, dest, configType) },
|
||||
"Request getConfig error",
|
||||
configType
|
||||
)
|
||||
|
||||
fun getModuleConfig(destNum: Int, configType: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getModuleConfig(packetId, dest, configType) },
|
||||
"Request getModuleConfig error",
|
||||
configType
|
||||
)
|
||||
|
||||
fun setRingtone(destNum: Int, ringtone: String) {
|
||||
meshService?.setRingtone(destNum, ringtone)
|
||||
}
|
||||
|
||||
fun getRingtone(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRingtone(packetId, dest) },
|
||||
"Request getRingtone error"
|
||||
)
|
||||
|
||||
fun setCannedMessages(destNum: Int, messages: String) {
|
||||
meshService?.setCannedMessages(destNum, messages)
|
||||
}
|
||||
|
||||
fun getCannedMessages(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getCannedMessages(packetId, dest) },
|
||||
"Request getCannedMessages error"
|
||||
)
|
||||
|
||||
fun requestTraceroute(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestTraceroute(packetId, dest) },
|
||||
"Request traceroute error"
|
||||
)
|
||||
|
||||
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
try {
|
||||
meshService?.requestPosition(destNum, position)
|
||||
|
@ -325,10 +368,6 @@ class UIViewModel @Inject constructor(
|
|||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val isRouter: Boolean = config.device.role == Config.DeviceConfig.Role.ROUTER
|
||||
|
||||
// We consider hasWifi = ESP32
|
||||
fun hasGPS() = myNodeInfo.value?.hasGPS == true
|
||||
fun hasWifi() = myNodeInfo.value?.hasWifi == true
|
||||
|
||||
/// hardware info about our local device (can be null)
|
||||
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
|
||||
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
|
||||
|
@ -361,93 +400,22 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
inline fun updateDeviceConfig(crossinline body: (Config.DeviceConfig) -> Config.DeviceConfig) {
|
||||
val data = body(config.device)
|
||||
setConfig(config { device = data })
|
||||
}
|
||||
|
||||
inline fun updatePositionConfig(crossinline body: (Config.PositionConfig) -> Config.PositionConfig) {
|
||||
val data = body(config.position)
|
||||
setConfig(config { position = data })
|
||||
}
|
||||
|
||||
inline fun updatePowerConfig(crossinline body: (Config.PowerConfig) -> Config.PowerConfig) {
|
||||
val data = body(config.power)
|
||||
setConfig(config { power = data })
|
||||
}
|
||||
|
||||
inline fun updateNetworkConfig(crossinline body: (Config.NetworkConfig) -> Config.NetworkConfig) {
|
||||
val data = body(config.network)
|
||||
setConfig(config { network = data })
|
||||
}
|
||||
|
||||
inline fun updateDisplayConfig(crossinline body: (Config.DisplayConfig) -> Config.DisplayConfig) {
|
||||
val data = body(config.display)
|
||||
setConfig(config { display = data })
|
||||
}
|
||||
|
||||
inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
||||
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
||||
val data = body(config.lora)
|
||||
setConfig(config { lora = data })
|
||||
}
|
||||
|
||||
inline fun updateBluetoothConfig(crossinline body: (Config.BluetoothConfig) -> Config.BluetoothConfig) {
|
||||
val data = body(config.bluetooth)
|
||||
setConfig(config { bluetooth = data })
|
||||
}
|
||||
|
||||
// Set the radio config (also updates our saved copy in preferences)
|
||||
fun setConfig(config: Config) {
|
||||
meshService?.setConfig(config.toByteArray())
|
||||
}
|
||||
|
||||
inline fun updateMQTTConfig(crossinline body: (ModuleConfig.MQTTConfig) -> ModuleConfig.MQTTConfig) {
|
||||
val data = body(module.mqtt)
|
||||
setModuleConfig(moduleConfig { mqtt = data })
|
||||
fun setRemoteConfig(destNum: Int, config: Config) {
|
||||
meshService?.setRemoteConfig(destNum, config.toByteArray())
|
||||
}
|
||||
|
||||
inline fun updateSerialConfig(crossinline body: (ModuleConfig.SerialConfig) -> ModuleConfig.SerialConfig) {
|
||||
val data = body(module.serial)
|
||||
setModuleConfig(moduleConfig { serial = data })
|
||||
}
|
||||
|
||||
inline fun updateExternalNotificationConfig(crossinline body: (ModuleConfig.ExternalNotificationConfig) -> ModuleConfig.ExternalNotificationConfig) {
|
||||
val data = body(module.externalNotification)
|
||||
setModuleConfig(moduleConfig { externalNotification = data })
|
||||
}
|
||||
|
||||
inline fun updateStoreForwardConfig(crossinline body: (ModuleConfig.StoreForwardConfig) -> ModuleConfig.StoreForwardConfig) {
|
||||
val data = body(module.storeForward)
|
||||
setModuleConfig(moduleConfig { storeForward = data })
|
||||
}
|
||||
|
||||
inline fun updateRangeTestConfig(crossinline body: (ModuleConfig.RangeTestConfig) -> ModuleConfig.RangeTestConfig) {
|
||||
val data = body(module.rangeTest)
|
||||
setModuleConfig(moduleConfig { rangeTest = data })
|
||||
}
|
||||
|
||||
inline fun updateTelemetryConfig(crossinline body: (ModuleConfig.TelemetryConfig) -> ModuleConfig.TelemetryConfig) {
|
||||
val data = body(module.telemetry)
|
||||
setModuleConfig(moduleConfig { telemetry = data })
|
||||
}
|
||||
|
||||
inline fun updateCannedMessageConfig(crossinline body: (ModuleConfig.CannedMessageConfig) -> ModuleConfig.CannedMessageConfig) {
|
||||
val data = body(module.cannedMessage)
|
||||
setModuleConfig(moduleConfig { cannedMessage = data })
|
||||
}
|
||||
|
||||
inline fun updateAudioConfig(crossinline body: (ModuleConfig.AudioConfig) -> ModuleConfig.AudioConfig) {
|
||||
val data = body(module.audio)
|
||||
setModuleConfig(moduleConfig { audio = data })
|
||||
}
|
||||
|
||||
inline fun updateRemoteHardwareConfig(crossinline body: (ModuleConfig.RemoteHardwareConfig) -> ModuleConfig.RemoteHardwareConfig) {
|
||||
val data = body(module.remoteHardware)
|
||||
setModuleConfig(moduleConfig { remoteHardware = data })
|
||||
}
|
||||
|
||||
fun setModuleConfig(config: ModuleConfig) {
|
||||
meshService?.setModuleConfig(config.toByteArray())
|
||||
fun setModuleConfig(destNum: Int, config: ModuleConfig) {
|
||||
meshService?.setModuleConfig(destNum, config.toByteArray())
|
||||
}
|
||||
|
||||
/// Convert the channels array to and from [AppOnlyProtos.ChannelSet]
|
||||
|
@ -518,6 +486,15 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setRemoteOwner(destNum: Int, user: User) {
|
||||
try {
|
||||
// Note: we use ?. here because we might be running in the emulator
|
||||
meshService?.setRemoteOwner(destNum, user.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Can't set username on device, is device offline? ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val adminChannelIndex: Int
|
||||
get() = channelSet.settingsList.map { it.name.lowercase() }.indexOf("admin")
|
||||
|
||||
|
@ -584,7 +561,7 @@ class UIViewModel @Inject constructor(
|
|||
meshLogRepository.getAllLogsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
|
||||
// If we get a NodeInfo packet, use it to update our position data (if valid)
|
||||
packet.nodeInfo?.let { nodeInfo ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let { _ ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let {
|
||||
nodePositions[nodeInfo.num] = nodeInfo.position
|
||||
}
|
||||
}
|
||||
|
@ -592,7 +569,7 @@ class UIViewModel @Inject constructor(
|
|||
packet.meshPacket?.let { proto ->
|
||||
// If the packet contains position data then use it to update, if valid
|
||||
packet.position?.let { position ->
|
||||
positionToPos.invoke(position)?.let { _ ->
|
||||
positionToPos.invoke(position)?.let {
|
||||
nodePositions[proto.from] = position
|
||||
}
|
||||
}
|
||||
|
|
|
@ -481,8 +481,7 @@ class MeshService : Service(), Logging {
|
|||
*
|
||||
* If id is null we assume a broadcast message
|
||||
*/
|
||||
private fun newMeshPacketTo(id: String) =
|
||||
newMeshPacketTo(toNodeNum(id))
|
||||
private fun newMeshPacketTo(id: String) = newMeshPacketTo(toNodeNum(id))
|
||||
|
||||
/**
|
||||
* Helper to make it easy to build a subpacket in the proper protobufs
|
||||
|
@ -512,9 +511,11 @@ class MeshService : Service(), Logging {
|
|||
* Helper to make it easy to build a subpacket in the proper protobufs
|
||||
*/
|
||||
private fun MeshPacket.Builder.buildAdminPacket(
|
||||
id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
|
||||
wantResponse: Boolean = false,
|
||||
initFn: AdminProtos.AdminMessage.Builder.() -> Unit
|
||||
): MeshPacket = buildMeshPacket(
|
||||
id = id,
|
||||
wantAck = true,
|
||||
channel = adminChannelIndex,
|
||||
priority = MeshPacket.Priority.RELIABLE
|
||||
|
@ -1480,28 +1481,6 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/** Send our current radio config to the device
|
||||
*/
|
||||
private fun setConfig(config: ConfigProtos.Config) {
|
||||
if (deviceVersion < minDeviceVersion) return
|
||||
debug("Setting new radio config!")
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
||||
setConfig = config
|
||||
})
|
||||
setLocalConfig(config) // Update our local copy
|
||||
}
|
||||
|
||||
/** Send our current module config to the device
|
||||
*/
|
||||
private fun setModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
||||
if (deviceVersion < minDeviceVersion) return
|
||||
debug("Setting new module config!")
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
||||
setModuleConfig = config
|
||||
})
|
||||
setLocalModuleConfig(config) // Update our local copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Send setOwner admin packet with [MeshProtos.User] protobuf
|
||||
*/
|
||||
|
@ -1639,6 +1618,19 @@ class MeshService : Service(), Logging {
|
|||
this@MeshService.setOwner(user)
|
||||
}
|
||||
|
||||
override fun setRemoteOwner(destNum: Int, payload: ByteArray) = toRemoteExceptions {
|
||||
val parsed = MeshProtos.User.parseFrom(payload)
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
||||
setOwner = parsed
|
||||
})
|
||||
}
|
||||
|
||||
override fun getRemoteOwner(id: Int, destNum: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
||||
getOwnerRequest = true
|
||||
})
|
||||
}
|
||||
|
||||
override fun send(p: DataPacket) {
|
||||
toRemoteExceptions {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
|
@ -1680,14 +1672,62 @@ class MeshService : Service(), Logging {
|
|||
this@MeshService.localConfig.toByteArray() ?: throw NoDeviceConfigException()
|
||||
}
|
||||
|
||||
/** Send our current radio config to the device
|
||||
*/
|
||||
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
|
||||
val parsed = ConfigProtos.Config.parseFrom(payload)
|
||||
setConfig(parsed)
|
||||
setRemoteConfig(myNodeNum, payload)
|
||||
}
|
||||
|
||||
override fun setModuleConfig(payload: ByteArray) = toRemoteExceptions {
|
||||
val parsed = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
||||
setModuleConfig(parsed)
|
||||
override fun setRemoteConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions {
|
||||
debug("Setting new radio config!")
|
||||
val config = ConfigProtos.Config.parseFrom(payload)
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setConfig = config })
|
||||
if (destNum == myNodeNum) setLocalConfig(config) // Update our local copy
|
||||
}
|
||||
|
||||
override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
||||
getConfigRequestValue = config
|
||||
})
|
||||
}
|
||||
|
||||
/** Send our current module config to the device
|
||||
*/
|
||||
override fun setModuleConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions {
|
||||
debug("Setting new module config!")
|
||||
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setModuleConfig = config })
|
||||
if (destNum == myNodeNum) setLocalModuleConfig(config) // Update our local copy
|
||||
}
|
||||
|
||||
override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
||||
getModuleConfigRequestValue = config
|
||||
})
|
||||
}
|
||||
|
||||
override fun setRingtone(destNum: Int, ringtone: String) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
||||
setRingtoneMessage = ringtone
|
||||
})
|
||||
}
|
||||
|
||||
override fun getRingtone(id: Int, destNum: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
||||
getRingtoneRequest = true
|
||||
})
|
||||
}
|
||||
|
||||
override fun setCannedMessages(destNum: Int, messages: String) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
||||
setCannedMessageModuleMessages = messages
|
||||
})
|
||||
}
|
||||
|
||||
override fun getCannedMessages(id: Int, destNum: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
||||
getCannedMessageModuleMessagesRequest = true
|
||||
})
|
||||
}
|
||||
|
||||
override fun setChannel(payload: ByteArray?) = toRemoteExceptions {
|
||||
|
@ -1732,14 +1772,16 @@ class MeshService : Service(), Logging {
|
|||
stopLocationRequests()
|
||||
}
|
||||
|
||||
override fun requestPosition(idNum: Int, position: Position) =
|
||||
toRemoteExceptions {
|
||||
val (lat, lon, alt) = position
|
||||
override fun requestPosition(destNum: Int, position: Position) = toRemoteExceptions {
|
||||
if (position == Position(0.0, 0.0, 0)) {
|
||||
// request position
|
||||
if (idNum != 0) sendPosition(time = 1, destNum = idNum, wantResponse = true)
|
||||
// set local node's fixed position
|
||||
else sendPosition(time = 0, destNum = null, lat = lat, lon = lon, alt = alt)
|
||||
sendPosition(time = 1, destNum = destNum, wantResponse = true)
|
||||
} else {
|
||||
// send fixed position
|
||||
val (lat, lon, alt) = position
|
||||
sendPosition(time = 0, destNum = null, lat = lat, lon = lon, alt = alt)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildMeshPacket(id = requestId) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -12,18 +13,23 @@ import androidx.compose.foundation.layout.wrapContentSize
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.KeyboardArrowRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -33,15 +39,25 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.AdminProtos.AdminMessage.ConfigType
|
||||
import com.geeksville.mesh.AdminProtos.AdminMessage.ModuleConfigType
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.TextDividerPreference
|
||||
import com.geeksville.mesh.ui.components.config.AudioConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.BluetoothConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.CannedMessageConfigItemList
|
||||
|
@ -63,7 +79,7 @@ import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DeviceSettingsFragment : ScreenFragment("Device Settings"), Logging {
|
||||
class DeviceSettingsFragment(val node: NodeInfo) : ScreenFragment("Device Settings"), Logging {
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
|
@ -77,139 +93,240 @@ class DeviceSettingsFragment : ScreenFragment("Device Settings"), Logging {
|
|||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
||||
setContent {
|
||||
AppCompatTheme {
|
||||
RadioConfigNavHost(model)
|
||||
RadioConfigNavHost(node, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ConfigDest(val title: String, val route: String) {
|
||||
USER("User", "user"),
|
||||
DEVICE("Device", "device"),
|
||||
POSITION("Position", "position"),
|
||||
POWER("Power", "power"),
|
||||
NETWORK("Network", "network"),
|
||||
DISPLAY("Display", "display"),
|
||||
LORA("LoRa", "lora"),
|
||||
BLUETOOTH("Bluetooth", "bluetooth")
|
||||
enum class ConfigDest(val title: String, val route: String, val config: ConfigType) {
|
||||
USER("User", "user", ConfigType.UNRECOGNIZED),
|
||||
DEVICE("Device", "device", ConfigType.DEVICE_CONFIG),
|
||||
POSITION("Position", "position", ConfigType.POSITION_CONFIG),
|
||||
POWER("Power", "power", ConfigType.POWER_CONFIG),
|
||||
NETWORK("Network", "network", ConfigType.NETWORK_CONFIG),
|
||||
DISPLAY("Display", "display", ConfigType.DISPLAY_CONFIG),
|
||||
LORA("LoRa", "lora", ConfigType.LORA_CONFIG),
|
||||
BLUETOOTH("Bluetooth", "bluetooth", ConfigType.BLUETOOTH_CONFIG);
|
||||
}
|
||||
|
||||
enum class ModuleDest(val title: String, val route: String) {
|
||||
MQTT("MQTT", "mqtt"),
|
||||
SERIAL("Serial", "serial"),
|
||||
EXT_NOTIFICATION("External Notification", "ext_notification"),
|
||||
STORE_FORWARD("Store & Forward", "store_forward"),
|
||||
RANGE_TEST("Range Test", "range_test"),
|
||||
TELEMETRY("Telemetry", "telemetry"),
|
||||
CANNED_MESSAGE("Canned Message", "canned_message"),
|
||||
AUDIO("Audio", "audio"),
|
||||
REMOTE_HARDWARE("Remote Hardware", "remote_hardware")
|
||||
enum class ModuleDest(val title: String, val route: String, val config: ModuleConfigType) {
|
||||
MQTT("MQTT", "mqtt", ModuleConfigType.MQTT_CONFIG),
|
||||
SERIAL("Serial", "serial", ModuleConfigType.SERIAL_CONFIG),
|
||||
EXTERNAL_NOTIFICATION("External Notification", "ext_not", ModuleConfigType.EXTNOTIF_CONFIG),
|
||||
STORE_FORWARD("Store & Forward", "store_forward", ModuleConfigType.STOREFORWARD_CONFIG),
|
||||
RANGE_TEST("Range Test", "range_test", ModuleConfigType.RANGETEST_CONFIG),
|
||||
TELEMETRY("Telemetry", "telemetry", ModuleConfigType.TELEMETRY_CONFIG),
|
||||
CANNED_MESSAGE("Canned Message", "canned_message", ModuleConfigType.CANNEDMSG_CONFIG),
|
||||
AUDIO("Audio", "audio", ModuleConfigType.AUDIO_CONFIG),
|
||||
REMOTE_HARDWARE("Remote Hardware", "remote_hardware", ModuleConfigType.REMOTEHARDWARE_CONFIG);
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
||||
fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) {
|
||||
val navController = rememberNavController()
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val connectionState by viewModel.connectionState.observeAsState()
|
||||
val connected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||
|
||||
val ourNodeInfo by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val localConfig by viewModel.localConfig.collectAsStateWithLifecycle()
|
||||
val moduleConfig by viewModel.moduleConfig.collectAsStateWithLifecycle()
|
||||
val destNum = node.num
|
||||
|
||||
var userConfig by remember { mutableStateOf(MeshProtos.User.getDefaultInstance()) }
|
||||
var radioConfig by remember { mutableStateOf(Config.getDefaultInstance()) }
|
||||
var moduleConfig by remember { mutableStateOf(ModuleConfig.getDefaultInstance()) }
|
||||
|
||||
var location by remember(node) { mutableStateOf(node.position) }
|
||||
var ringtone by remember { mutableStateOf("") }
|
||||
var cannedMessageMessages by remember { mutableStateOf("") }
|
||||
|
||||
val configResponse by viewModel.packetResponse.collectAsStateWithLifecycle()
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(configResponse) {
|
||||
val data = configResponse?.meshPacket?.decoded
|
||||
if (isWaiting && data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
|
||||
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
|
||||
when (parsed.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
val response = parsed.getChannelResponse // TODO
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> {
|
||||
isWaiting = false
|
||||
userConfig = parsed.getOwnerResponse
|
||||
navController.navigate("user")
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
||||
isWaiting = false
|
||||
val response = parsed.getConfigResponse
|
||||
radioConfig = response
|
||||
enumValues<ConfigDest>().find { it.name == "${response.payloadVariantCase}" }?.let {
|
||||
navController.navigate(it.route)
|
||||
}
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> {
|
||||
isWaiting = false
|
||||
val response = parsed.getModuleConfigResponse
|
||||
moduleConfig = response
|
||||
enumValues<ModuleDest>().find { it.name == "${response.payloadVariantCase}" }?.let {
|
||||
navController.navigate(it.route)
|
||||
}
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> {
|
||||
cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse
|
||||
viewModel.getModuleConfig(destNum, ModuleConfigType.CANNEDMSG_CONFIG_VALUE)
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> {
|
||||
ringtone = parsed.getRingtoneResponse
|
||||
viewModel.getModuleConfig(destNum, ModuleConfigType.EXTNOTIF_CONFIG_VALUE)
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(navController = navController, startDestination = "home") {
|
||||
composable("home") { RadioSettingsScreen(navController) }
|
||||
composable("home") {
|
||||
RadioSettingsScreen(
|
||||
enabled = connected && !isWaiting,
|
||||
headerText = node.user?.longName ?: stringResource(R.string.unknown_username),
|
||||
onRouteClick = { configType ->
|
||||
isWaiting = true
|
||||
// clearAllConfigs() ?
|
||||
when (configType) {
|
||||
ConfigType.UNRECOGNIZED -> {
|
||||
viewModel.getOwner(destNum)
|
||||
}
|
||||
is ConfigType -> {
|
||||
viewModel.getConfig(destNum, configType.number)
|
||||
}
|
||||
ModuleConfigType.CANNEDMSG_CONFIG -> {
|
||||
viewModel.getCannedMessages(destNum)
|
||||
}
|
||||
ModuleConfigType.EXTNOTIF_CONFIG -> {
|
||||
viewModel.getRingtone(destNum)
|
||||
}
|
||||
is ModuleConfigType -> {
|
||||
viewModel.getModuleConfig(destNum, configType.number)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
composable("user") {
|
||||
UserConfigItemList(
|
||||
userConfig = ourNodeInfo?.user!!,
|
||||
userConfig = userConfig,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { userInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.setOwner(userInput)
|
||||
viewModel.setRemoteOwner(destNum, userInput)
|
||||
userConfig = userInput
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("device") {
|
||||
DeviceConfigItemList(
|
||||
deviceConfig = localConfig.device,
|
||||
deviceConfig = radioConfig.device,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { deviceInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateDeviceConfig { deviceInput }
|
||||
val config = config { device = deviceInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("position") {
|
||||
PositionConfigItemList(
|
||||
positionInfo = ourNodeInfo?.position,
|
||||
positionConfig = localConfig.position,
|
||||
locationInfo = location,
|
||||
positionConfig = radioConfig.position,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { positionPair ->
|
||||
focusManager.clearFocus()
|
||||
val (locationInput, positionInput) = positionPair
|
||||
if (locationInput != ourNodeInfo?.position && positionInput.fixedPosition)
|
||||
locationInput?.let { viewModel.requestPosition(0, it) }
|
||||
if (positionInput != localConfig.position) viewModel.updatePositionConfig { positionInput }
|
||||
if (locationInput != node.position && positionInput.fixedPosition) {
|
||||
locationInput?.let { viewModel.requestPosition(destNum, it) }
|
||||
location = locationInput
|
||||
}
|
||||
if (positionInput != radioConfig.position) {
|
||||
val config = config { position = positionInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("power") {
|
||||
PowerConfigItemList(
|
||||
powerConfig = localConfig.power,
|
||||
powerConfig = radioConfig.power,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { powerInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updatePowerConfig { powerInput }
|
||||
val config = config { power = powerInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("network") {
|
||||
NetworkConfigItemList(
|
||||
networkConfig = localConfig.network,
|
||||
networkConfig = radioConfig.network,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { networkInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateNetworkConfig { networkInput }
|
||||
val config = config { network = networkInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("display") {
|
||||
DisplayConfigItemList(
|
||||
displayConfig = localConfig.display,
|
||||
displayConfig = radioConfig.display,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { displayInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateDisplayConfig { displayInput }
|
||||
val config = config { display = displayInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("lora") {
|
||||
LoRaConfigItemList(
|
||||
loraConfig = localConfig.lora,
|
||||
loraConfig = radioConfig.lora,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { loraInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateLoraConfig { loraInput }
|
||||
val config = config { lora = loraInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("bluetooth") {
|
||||
BluetoothConfigItemList(
|
||||
bluetoothConfig = localConfig.bluetooth,
|
||||
bluetoothConfig = radioConfig.bluetooth,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { bluetoothInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateBluetoothConfig { bluetoothInput }
|
||||
val config = config { bluetooth = bluetoothInput }
|
||||
viewModel.setRemoteConfig(destNum, config)
|
||||
radioConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -220,7 +337,9 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { mqttInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateMQTTConfig { mqttInput }
|
||||
val config = moduleConfig { mqtt = mqttInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -231,18 +350,30 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { serialInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateSerialConfig { serialInput }
|
||||
val config = moduleConfig { serial = serialInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("ext_notification") {
|
||||
composable("ext_not") {
|
||||
ExternalNotificationConfigItemList(
|
||||
externalNotificationConfig = moduleConfig.externalNotification,
|
||||
ringtone = ringtone,
|
||||
extNotificationConfig = moduleConfig.externalNotification,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { externalNotificationInput ->
|
||||
onSaveClicked = { extNotificationPair ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateExternalNotificationConfig { externalNotificationInput }
|
||||
val (ringtoneInput, extNotificationInput) = extNotificationPair
|
||||
if (ringtoneInput != ringtone) {
|
||||
viewModel.setRingtone(destNum, ringtoneInput)
|
||||
ringtone = ringtoneInput
|
||||
}
|
||||
if (extNotificationInput != moduleConfig.externalNotification) {
|
||||
val config = moduleConfig { externalNotification = extNotificationInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -253,7 +384,9 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { storeForwardInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateStoreForwardConfig { storeForwardInput }
|
||||
val config = moduleConfig { storeForward = storeForwardInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -264,7 +397,9 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { rangeTestInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateRangeTestConfig { rangeTestInput }
|
||||
val config = moduleConfig { rangeTest = rangeTestInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -275,18 +410,30 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { telemetryInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateTelemetryConfig { telemetryInput }
|
||||
val config = moduleConfig { telemetry = telemetryInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("canned_message") {
|
||||
CannedMessageConfigItemList(
|
||||
messages = cannedMessageMessages,
|
||||
cannedMessageConfig = moduleConfig.cannedMessage,
|
||||
enabled = connected,
|
||||
focusManager = focusManager,
|
||||
onSaveClicked = { cannedMessageInput ->
|
||||
onSaveClicked = { cannedMessagePair ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateCannedMessageConfig { cannedMessageInput }
|
||||
val (messagesInput, cannedMessageInput) = cannedMessagePair
|
||||
if (messagesInput != cannedMessageMessages) {
|
||||
viewModel.setCannedMessages(destNum, messagesInput)
|
||||
cannedMessageMessages = messagesInput
|
||||
}
|
||||
if (cannedMessageInput != moduleConfig.cannedMessage) {
|
||||
val config = moduleConfig { cannedMessage = cannedMessageInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -297,7 +444,9 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { audioInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateAudioConfig { audioInput }
|
||||
val config = moduleConfig { audio = audioInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -308,7 +457,9 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
focusManager = focusManager,
|
||||
onSaveClicked = { remoteHardwareInput ->
|
||||
focusManager.clearFocus()
|
||||
viewModel.updateRemoteHardwareConfig { remoteHardwareInput }
|
||||
val config = moduleConfig { remoteHardware = remoteHardwareInput }
|
||||
viewModel.setModuleConfig(destNum, config)
|
||||
moduleConfig = config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -316,12 +467,16 @@ fun RadioConfigNavHost(viewModel: UIViewModel = viewModel()) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun NavCard(title: String, onClick: () -> Unit) {
|
||||
fun NavCard(
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp, horizontal = 16.dp)
|
||||
.clickable { onClick() },
|
||||
.padding(vertical = 2.dp)
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
elevation = 4.dp
|
||||
) {
|
||||
Row(
|
||||
|
@ -331,6 +486,7 @@ fun NavCard(title: String, onClick: () -> Unit) {
|
|||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
|
@ -341,32 +497,43 @@ fun NavCard(title: String, onClick: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun RadioSettingsScreen(navController: NavHostController) {
|
||||
LazyColumn {
|
||||
item {
|
||||
PreferenceCategory(
|
||||
stringResource(id = R.string.device_settings), Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
items(ConfigDest.values()) { configs ->
|
||||
NavCard(configs.title) { navController.navigate(configs.route) }
|
||||
fun RadioSettingsScreen(
|
||||
enabled: Boolean = true,
|
||||
headerText: String = "longName",
|
||||
onRouteClick: (Any) -> Unit = {},
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
stickyHeader {
|
||||
TextDividerPreference(headerText)
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceCategory(
|
||||
stringResource(id = R.string.module_settings), Modifier.padding(horizontal = 16.dp)
|
||||
stringResource(id = R.string.device_settings)
|
||||
)
|
||||
}
|
||||
items(ConfigDest.values()) { configs ->
|
||||
NavCard(configs.title, enabled = enabled) { onRouteClick(configs.config) }
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceCategory(
|
||||
stringResource(id = R.string.module_settings)
|
||||
)
|
||||
}
|
||||
|
||||
items(ModuleDest.values()) { modules ->
|
||||
NavCard(modules.title) { navController.navigate(modules.route) }
|
||||
NavCard(modules.title, enabled = enabled) { onRouteClick(modules.config) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun RadioSettingsScreenPreview(){
|
||||
RadioSettingsScreen(NavHostController(LocalContext.current))
|
||||
fun RadioSettingsScreenPreview() {
|
||||
RadioSettingsScreen()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import androidx.lifecycle.asLiveData
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
|
||||
|
@ -56,6 +57,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
private var nodes = arrayOf<NodeInfo>()
|
||||
|
||||
private fun popup(view: View, position: Int) {
|
||||
if (!model.isConnected()) return
|
||||
val node = nodes[position]
|
||||
val user = node.user
|
||||
val showAdmin = position == 0 || model.adminChannelIndex > 0
|
||||
|
@ -93,6 +95,13 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
model.requestTraceroute(node.num)
|
||||
}
|
||||
}
|
||||
R.id.remote_admin -> {
|
||||
debug("calling remote admin --> destNum: ${node.num}")
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(R.id.mainActivityLayout, DeviceSettingsFragment(node))
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
R.id.reboot -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("${getString(R.string.reboot)}\n${user?.longName}?")
|
||||
|
@ -333,6 +342,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
|
||||
model.packetResponse.asLiveData().observe(viewLifecycleOwner) { meshLog ->
|
||||
meshLog?.meshPacket?.let { meshPacket ->
|
||||
if (meshPacket.decoded.portnum != Portnums.PortNum.TRACEROUTE_APP) return@let
|
||||
val routeList = meshLog.routeDiscovery?.routeList
|
||||
fun nodeName(num: Int) = model.nodeDB.nodesByNum?.get(num)?.user?.longName
|
||||
|
||||
|
|
|
@ -26,11 +26,13 @@ import com.geeksville.mesh.ui.components.SwitchPreference
|
|||
|
||||
@Composable
|
||||
fun CannedMessageConfigItemList(
|
||||
messages: String,
|
||||
cannedMessageConfig: CannedMessageConfig,
|
||||
enabled: Boolean,
|
||||
focusManager: FocusManager,
|
||||
onSaveClicked: (CannedMessageConfig) -> Unit,
|
||||
onSaveClicked: (Pair<String, CannedMessageConfig>) -> Unit,
|
||||
) {
|
||||
var messagesInput by remember(messages) { mutableStateOf(messages) }
|
||||
var cannedMessageInput by remember(cannedMessageConfig) { mutableStateOf(cannedMessageConfig) }
|
||||
|
||||
LazyColumn(
|
||||
|
@ -162,14 +164,29 @@ fun CannedMessageConfigItemList(
|
|||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Messages",
|
||||
value = messagesInput,
|
||||
maxSize = 200, // messages max_size:201
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { messagesInput = it }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = cannedMessageInput != cannedMessageConfig,
|
||||
enabled = cannedMessageInput != cannedMessageConfig || messagesInput != messages,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
messagesInput = messages
|
||||
cannedMessageInput = cannedMessageConfig
|
||||
},
|
||||
onSaveClicked = { onSaveClicked(cannedMessageInput) }
|
||||
onSaveClicked = { onSaveClicked(Pair(messagesInput,cannedMessageInput)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +196,7 @@ fun CannedMessageConfigItemList(
|
|||
@Composable
|
||||
fun CannedMessageConfigPreview(){
|
||||
CannedMessageConfigItemList(
|
||||
messages = "",
|
||||
cannedMessageConfig = CannedMessageConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
focusManager = LocalFocusManager.current,
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.geeksville.mesh.ui.components.config
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -12,6 +13,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
|
||||
import com.geeksville.mesh.copy
|
||||
|
@ -23,13 +26,15 @@ import com.geeksville.mesh.ui.components.TextDividerPreference
|
|||
|
||||
@Composable
|
||||
fun ExternalNotificationConfigItemList(
|
||||
externalNotificationConfig: ExternalNotificationConfig,
|
||||
ringtone: String,
|
||||
extNotificationConfig: ExternalNotificationConfig,
|
||||
enabled: Boolean,
|
||||
focusManager: FocusManager,
|
||||
onSaveClicked: (ExternalNotificationConfig) -> Unit,
|
||||
onSaveClicked: (Pair<String, ExternalNotificationConfig>) -> Unit,
|
||||
) {
|
||||
var externalNotificationInput by remember(externalNotificationConfig) {
|
||||
mutableStateOf(externalNotificationConfig)
|
||||
var ringtoneInput by remember(ringtone) { mutableStateOf(ringtone) }
|
||||
var externalNotificationInput by remember(extNotificationConfig) {
|
||||
mutableStateOf(extNotificationConfig)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
|
@ -183,14 +188,29 @@ fun ExternalNotificationConfigItemList(
|
|||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Ringtone",
|
||||
value = ringtoneInput,
|
||||
maxSize = 230, // ringtone max_size:231
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { ringtoneInput = it }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = externalNotificationInput != externalNotificationConfig,
|
||||
enabled = externalNotificationInput != extNotificationConfig || ringtoneInput != ringtone,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
externalNotificationInput = externalNotificationConfig
|
||||
ringtoneInput = ringtone
|
||||
externalNotificationInput = extNotificationConfig
|
||||
},
|
||||
onSaveClicked = { onSaveClicked(externalNotificationInput) }
|
||||
onSaveClicked = { onSaveClicked(Pair(ringtoneInput, externalNotificationInput)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +220,8 @@ fun ExternalNotificationConfigItemList(
|
|||
@Composable
|
||||
fun ExternalNotificationConfigPreview(){
|
||||
ExternalNotificationConfigItemList(
|
||||
externalNotificationConfig = ExternalNotificationConfig.getDefaultInstance(),
|
||||
ringtone = "",
|
||||
extNotificationConfig = ExternalNotificationConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
focusManager = LocalFocusManager.current,
|
||||
onSaveClicked = { },
|
||||
|
|
|
@ -25,13 +25,13 @@ import com.geeksville.mesh.ui.components.SwitchPreference
|
|||
|
||||
@Composable
|
||||
fun PositionConfigItemList(
|
||||
positionInfo: Position?,
|
||||
locationInfo: Position?,
|
||||
positionConfig: PositionConfig,
|
||||
enabled: Boolean,
|
||||
focusManager: FocusManager,
|
||||
onSaveClicked: (Pair<Position?, PositionConfig>) -> Unit,
|
||||
) {
|
||||
var locationInput by remember(positionInfo) { mutableStateOf(positionInfo) }
|
||||
var locationInput by remember(locationInfo) { mutableStateOf(locationInfo) }
|
||||
var positionInput by remember(positionConfig) { mutableStateOf(positionConfig) }
|
||||
|
||||
LazyColumn(
|
||||
|
@ -175,10 +175,10 @@ fun PositionConfigItemList(
|
|||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = positionInput != positionConfig || locationInput != positionInfo,
|
||||
enabled = positionInput != positionConfig || locationInput != locationInfo,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
locationInput = positionInfo
|
||||
locationInput = locationInfo
|
||||
positionInput = positionConfig
|
||||
},
|
||||
onSaveClicked = { onSaveClicked(Pair(locationInput, positionInput)) }
|
||||
|
@ -191,7 +191,7 @@ fun PositionConfigItemList(
|
|||
@Composable
|
||||
fun PositionConfigPreview(){
|
||||
PositionConfigItemList(
|
||||
positionInfo = null,
|
||||
locationInfo = null,
|
||||
positionConfig = PositionConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
focusManager = LocalFocusManager.current,
|
||||
|
|
|
@ -17,20 +17,21 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshUser
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.RegularPreference
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.geeksville.mesh.user
|
||||
|
||||
@Composable
|
||||
fun UserConfigItemList(
|
||||
userConfig: MeshUser,
|
||||
userConfig: MeshProtos.User,
|
||||
enabled: Boolean,
|
||||
focusManager: FocusManager,
|
||||
onSaveClicked: (MeshUser) -> Unit,
|
||||
onSaveClicked: (MeshProtos.User) -> Unit,
|
||||
) {
|
||||
var userInput by remember(userConfig) { mutableStateOf(userConfig) }
|
||||
|
||||
|
@ -57,9 +58,9 @@ fun UserConfigItemList(
|
|||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
userInput = userInput.copy(longName = it)
|
||||
userInput = userInput.copy { longName = it }
|
||||
if (getInitials(it).toByteArray().size <= 4) // short_name max_size:5
|
||||
userInput = userInput.copy(shortName = getInitials(it))
|
||||
userInput = userInput.copy { shortName = getInitials(it) }
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -73,7 +74,7 @@ fun UserConfigItemList(
|
|||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { userInput = userInput.copy(shortName = it) })
|
||||
onValueChanged = { userInput = userInput.copy { shortName = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
|
@ -87,7 +88,7 @@ fun UserConfigItemList(
|
|||
SwitchPreference(title = "Licensed amateur radio",
|
||||
checked = userInput.isLicensed,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { userInput = userInput.copy(isLicensed = it) })
|
||||
onCheckedChange = { userInput = userInput.copy { isLicensed = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
|
@ -107,13 +108,13 @@ fun UserConfigItemList(
|
|||
@Composable
|
||||
fun UserConfigPreview(){
|
||||
UserConfigItemList(
|
||||
userConfig = MeshUser(
|
||||
id = "!a280d9c8",
|
||||
longName = "Meshtastic d9c8",
|
||||
shortName = "d9c8",
|
||||
hwModel = MeshProtos.HardwareModel.RAK4631,
|
||||
userConfig = user {
|
||||
id = "!a280d9c8"
|
||||
longName = "Meshtastic d9c8"
|
||||
shortName = "d9c8"
|
||||
hwModel = MeshProtos.HardwareModel.RAK4631
|
||||
isLicensed = false
|
||||
),
|
||||
},
|
||||
enabled = true,
|
||||
focusManager = LocalFocusManager.current,
|
||||
onSaveClicked = { },
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
app:showAsAction="withText" />
|
||||
</group>
|
||||
<group android:id="@+id/group_admin">
|
||||
<item
|
||||
android:id="@+id/remote_admin"
|
||||
android:title="@string/device_settings"
|
||||
app:showAsAction="withText" />
|
||||
<item
|
||||
android:id="@+id/reboot"
|
||||
android:title="@string/reboot"
|
||||
|
|
|
@ -157,8 +157,8 @@
|
|||
<string name="map_start_download">Start Download</string>
|
||||
<string name="request_position">Request position</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="device_settings">Device settings</string>
|
||||
<string name="module_settings">Module settings</string>
|
||||
<string name="device_settings">Radio configuration</string>
|
||||
<string name="module_settings">Module configuration</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="calculating">Calculating…</string>
|
||||
<string name="map_offline_manager">Offline Manager</string>
|
||||
|
|
Ładowanie…
Reference in New Issue