mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-14 00:45:24 -04:00
feat(settings): implement set_ham_mode HamParameters admin message
When the licensed amateur radio toggle is on for the locally connected node, the User config screen repurposes the long-name field as the callsign (max 8 chars, iOS parity) and saving sends AdminMessage(set_ham_mode) instead of set_owner. Current LoRa tx_power/override_frequency are echoed into the HamParameters so a re-send while already licensed never wipes the node's overrides (firmware applies them verbatim). The node entry is optimistically updated pending the device's authoritative NodeInfo. Closes #5759 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@@ -40,6 +41,20 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables amateur-radio (ham) mode on the locally connected node via `set_ham_mode`. At protobufs 2.7.25 only
|
||||
* `call_sign` and `short_name` are user-supplied; `long_name` joins when meshtastic/protobufs#941 ships.
|
||||
*
|
||||
* @param destNum The node number to update (must be the local node).
|
||||
* @param hamParameters The ham onboarding parameters.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setHamMode(destNum: Int, hamParameters: HamParameters): Int {
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setHamMode(destNum, hamParameters, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the owner information from the radio.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.testing.FakeRadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -45,6 +46,12 @@ class RadioConfigUseCaseTest {
|
||||
// FakeRadioController already has getPacketId returning 1.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setHamMode calls radioController and returns packetId`() = runTest {
|
||||
val packetId = useCase.setHamMode(1234, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"))
|
||||
assertEquals(1, packetId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOwner calls radioController`() = runTest {
|
||||
val packetId = useCase.getOwner(1234)
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.meshtastic.core.repository
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@@ -52,6 +53,17 @@ interface AdminController {
|
||||
/** Updates the owner (user info) on a remote node. */
|
||||
suspend fun setOwner(destNum: Int, user: User, packetId: Int)
|
||||
|
||||
/**
|
||||
* Enables amateur-radio (ham) mode on a node via `AdminMessage.set_ham_mode`.
|
||||
*
|
||||
* Must only target the locally connected node — firmware ham onboarding is a local operation and the UI gates it
|
||||
* accordingly. The firmware handler rewrites the owner (long_name = call_sign), flips `is_licensed`, disables
|
||||
* encryption, applies [HamParameters.tx_power]/[HamParameters.frequency] to the LoRa config verbatim, and reboots,
|
||||
* so callers must echo the node's current LoRa values rather than send defaults. Intentionally absent from
|
||||
* [AdminEditScope]: ham enablement is not a batch-edit operation.
|
||||
*/
|
||||
suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int)
|
||||
|
||||
/** Updates the general configuration on a remote node. */
|
||||
suspend fun setConfig(destNum: Int, config: Config, packetId: Int)
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
<string name="button_gpio">Button GPIO</string>
|
||||
<string name="buzzer_gpio">Buzzer GPIO</string>
|
||||
<string name="calculating">Calculating…</string>
|
||||
<string name="call_sign">Call sign</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="cancel_reply">Cancel reply</string>
|
||||
<string name="canned_message">Canned Message</string>
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.meshtastic.core.service
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
@@ -30,6 +31,7 @@ import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.OTAMode
|
||||
import org.meshtastic.proto.User
|
||||
@@ -65,6 +67,23 @@ internal class AdminControllerImpl(
|
||||
commandSender.sendAdmin(destNum, packetId, wantResponse = true) { AdminMessage(get_owner_request = true) }
|
||||
}
|
||||
|
||||
override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) {
|
||||
// Firmware applies tx_power/frequency to the LoRa config verbatim, so echo the node's current
|
||||
// values to keep a re-send (e.g. a callsign edit while already licensed) from wiping overrides.
|
||||
val lora = radioConfigRepository.localConfigFlow.firstOrNull()?.lora
|
||||
val params = hamParameters.copy(tx_power = lora?.tx_power ?: 0, frequency = lora?.override_frequency ?: 0f)
|
||||
commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_ham_mode = params) }
|
||||
val currentUser = nodeManager.nodeDBbyNodeNum[destNum]?.user ?: User()
|
||||
nodeManager.handleReceivedUser(
|
||||
destNum,
|
||||
currentUser.copy(
|
||||
long_name = hamParameters.call_sign,
|
||||
short_name = hamParameters.short_name,
|
||||
is_licensed = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Configuration ─────────────────────────────────────────────────────────
|
||||
|
||||
override suspend fun setLocalConfig(config: Config) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.calls
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
@@ -49,7 +50,11 @@ import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.Test
|
||||
@@ -356,6 +361,39 @@ class RadioControllerImplTest {
|
||||
verify { nodeManager.handleReceivedUser(42, any(), any(), true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setHamModeSendsAdminWithEchoedLoraValuesAndUpdatesUser() = runTest {
|
||||
val controller = createController()
|
||||
val existingUser = User(id = "!0000007b", long_name = "Old Name", short_name = "OLD")
|
||||
every { nodeManager.nodeDBbyNodeNum } returns mapOf(123 to Node(num = 123, user = existingUser))
|
||||
every { radioConfigRepository.localConfigFlow } returns
|
||||
MutableStateFlow(LocalConfig(lora = Config.LoRaConfig(tx_power = 20, override_frequency = 915.5f)))
|
||||
|
||||
var sentMessage: AdminMessage? = null
|
||||
everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls
|
||||
{
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sentMessage = (it.args[3] as () -> AdminMessage)()
|
||||
}
|
||||
|
||||
controller.setHamMode(123, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"), 42)
|
||||
|
||||
val ham = sentMessage?.set_ham_mode
|
||||
assertEquals("KK7ABC", ham?.call_sign)
|
||||
assertEquals("KK7A", ham?.short_name)
|
||||
// Current LoRa values are echoed so a re-send never wipes the node's overrides.
|
||||
assertEquals(20, ham?.tx_power)
|
||||
assertEquals(915.5f, ham?.frequency)
|
||||
verify {
|
||||
nodeManager.handleReceivedUser(
|
||||
123,
|
||||
existingUser.copy(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true),
|
||||
0,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importContactReturnsEarlyWhenDisconnected() = runTest {
|
||||
val controller = createController(myNodeNum = null)
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HamParameters
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@@ -99,6 +100,8 @@ class FakeRadioController :
|
||||
|
||||
override suspend fun setOwner(destNum: Int, user: User, packetId: Int) {}
|
||||
|
||||
override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) {}
|
||||
|
||||
override suspend fun setConfig(destNum: Int, config: Config, packetId: Int) {}
|
||||
|
||||
override suspend fun setModuleConfig(destNum: Int, config: ModuleConfig, packetId: Int) {}
|
||||
|
||||
Reference in New Issue
Block a user