Document radio PTT ownership and talk event state contracts
This commit is contained in:
1 parent
43b14ece78
commit
95894273f1
2 files changed
+26
-5
No files matched your search
@@ -1,16 +1,25 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
/** Независимые владельцы ручной передачи. Все операции выполняются в главном потоке. */
|
||||
/** Источник ручной передачи: кнопка чата, плавающая кнопка либо общий владелец volume-клавиш. */
|
||||
internal enum class RadioPttSource { BUTTON, OVERLAY, VOLUME }
|
||||
|
||||
/**
|
||||
* Независимые удержания PTT без управления звуком и таймерами; все вызовы сериализует main-поток владельца.
|
||||
* Каждый источник занимает одно место: повтор press не увеличивает счётчик. Несколько физических клавиш
|
||||
* одного источника вызывающий агрегирует сам и делает release только после отпускания последней.
|
||||
* RadioAudioEngine использует release для хвоста передачи, cancel — для немедленной отмены без хвоста.
|
||||
*/
|
||||
internal class RadioPttHolds {
|
||||
private val held = mutableSetOf<RadioPttSource>()
|
||||
private val held = mutableSetOf<RadioPttSource>() // Только ручные владельцы; VAD/автоматика сюда не входят.
|
||||
|
||||
/** Добавить source идемпотентно; остальные владельцы сохраняются. */
|
||||
fun press(source: RadioPttSource) { held.add(source) }
|
||||
|
||||
/** Завершать передачу можно только после отпускания последнего владельца. */
|
||||
/** Удалить source; true только при удалении последнего владельца. Неизвестный/повторный release даёт false. */
|
||||
fun release(source: RadioPttSource): Boolean = held.remove(source) && held.isEmpty()
|
||||
|
||||
/** Удалить source либо всех при null; вернуть отсутствие владельцев, даже если набор уже был пуст.
|
||||
* Поэтому cancel разрешает снять отложенный хвост передачи также после предыдущего release. */
|
||||
fun cancel(source: RadioPttSource? = null): Boolean {
|
||||
if (source == null) held.clear() else held.remove(source)
|
||||
return held.isEmpty()
|
||||
|
||||
@@ -3,17 +3,29 @@ package com.utun.chat.data
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/** Эфемерные говорящие одной активной рации; снимки участников и локальный mute не меняют этот набор. */
|
||||
/**
|
||||
* Снимок говорящих одной рации: groupId=0 отключает обработку событий, speakers содержит node ID.
|
||||
* При включении/смене канала вызывающий создаёт новый RadioTalkState(group), при выключении — RadioTalkState().
|
||||
* Снимки участников и локальный mute независимы от индикации речи; переданный speakers нельзя мутировать извне.
|
||||
* C-ядро хранит одного talker на узел: смена stream даёт новый START без STOP прежнего stream.
|
||||
* Поэтому набор учитывает nodeId, а stream служит диагностике и не создаёт отдельного говорящего.
|
||||
* ID — ненулевые 64-битные значения; отрицательный Long сохраняет unsigned-биты протокольного ID.
|
||||
*/
|
||||
data class RadioTalkState(val groupId: Long = 0, val speakers: Set<Long> = emptySet()) {
|
||||
/** Применить валидное событие парсера; чужая/выключенная группа и повтор состояния возвращают this.
|
||||
* START добавляет узел, STOP удаляет только его; прежний снимок остаётся неизменным. */
|
||||
fun onTalk(event: RadioTalkEvent): RadioTalkState {
|
||||
if (groupId == 0L || event.groupId != groupId || speakers.contains(event.nodeId) == event.on) return this
|
||||
return copy(speakers = if (event.on) speakers + event.nodeId else speakers - event.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Одно событие речи: ненулевые ID группы/узла, stream=0..65535, on=true для START и false для STOP. */
|
||||
data class RadioTalkEvent(val groupId: Long, val nodeId: Long, val stream: Int, val on: Boolean)
|
||||
|
||||
/** Строгий формат CHAT_EVT_RADIO_TALK: group(8), source(8), stream(2), on(1), little endian. */
|
||||
/** Разобрать ровно 19 байтов CHAT_EVT_RADIO_TALK: group(8), source(8), stream(2), on(1), little endian.
|
||||
* Только on=0/1 и ненулевые group/source; stream допускает весь uint16, включая 0.
|
||||
* При null, неверной длине/флаге/нулевом ID вернуть null; диагностику отказа пишет вызывающий ChatViewModel. */
|
||||
fun parseRadioTalkEvent(data: ByteArray?): RadioTalkEvent? {
|
||||
if (data == null || data.size != 19 || data[18].toInt() !in 0..1) return null
|
||||
val bytes = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
Reference in new issue
Block a user