Add network map and packet diagnostics
Some checks failed
Android CI / build (push) Has been cancelled
Some checks failed
Android CI / build (push) Has been cancelled
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
- Реализованы главный экран со списком чатов, отдельный экран диалога, меню `три точки -> Настройки`, ручная проверка обновлений и опциональная автопроверка через `version.json`.
|
- Реализованы главный экран со списком чатов, отдельный экран диалога, меню `три точки -> Настройки`, ручная проверка обновлений и опциональная автопроверка через `version.json`.
|
||||||
- Добавлены профили пользователей: `firstName`, `lastName`, `username`, описание, локальное редактирование профиля и поиск профиля по `username`.
|
- Добавлены профили пользователей: `firstName`, `lastName`, `username`, описание, локальное редактирование профиля и поиск профиля по `username`.
|
||||||
- Профили распространяются как mesh-пакеты и кэшируются узлами локально; по найденному `username` можно получить `peerId`.
|
- Профили распространяются как mesh-пакеты и кэшируются узлами локально; по найденному `username` можно получить `peerId`.
|
||||||
|
- В настройки добавлены диагностические режимы: карта сети и журнал исходящих, входящих и транзитных пакетов.
|
||||||
- При выключенном Bluetooth приложение запрашивает его включение перед запуском mesh.
|
- При выключенном Bluetooth приложение запрашивает его включение перед запуском mesh.
|
||||||
- Публикация APK и сайта автоматизирована через `Makefile`.
|
- Публикация APK и сайта автоматизирована через `Makefile`.
|
||||||
- Проект и Android-приложение приведены к имени `NNNet`, пакет приложения: `pro.nnnteam.nnnet`.
|
- Проект и Android-приложение приведены к имени `NNNet`, пакет приложения: `pro.nnnteam.nnnet`.
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
|
|
||||||
4. **Data Layer**
|
4. **Data Layer**
|
||||||
- локальное хранилище (Room);
|
- локальное хранилище (Room);
|
||||||
- история сообщений, очередь исходящей доставки и каталог профилей.
|
- история сообщений, очередь исходящей доставки, каталог профилей и журнал пакетов.
|
||||||
|
|
||||||
5. **Security Layer**
|
5. **Security Layer**
|
||||||
- идентификация пользователя;
|
- идентификация пользователя;
|
||||||
@@ -85,6 +86,8 @@
|
|||||||
- [x] Подключить Room и базовую схему хранения.
|
- [x] Подключить Room и базовую схему хранения.
|
||||||
- [x] Реализовать базовую регистрацию пользователя (локальный профиль).
|
- [x] Реализовать базовую регистрацию пользователя (локальный профиль).
|
||||||
- [x] Добавить кэш профилей из mesh-сети и поиск по `username`.
|
- [x] Добавить кэш профилей из mesh-сети и поиск по `username`.
|
||||||
|
- [x] Добавить журнал исходящих, входящих и транзитных пакетов.
|
||||||
|
- [x] Добавить режим карты сети в настройках.
|
||||||
- [x] Добавить логирование сети и debug-экран маршрутов.
|
- [x] Добавить логирование сети и debug-экран маршрутов.
|
||||||
- [x] Добавить ручную проверку обновлений и опциональную автопроверку клиента.
|
- [x] Добавить ручную проверку обновлений и опциональную автопроверку клиента.
|
||||||
- [ ] Добавить шифрование полезной нагрузки сообщений.
|
- [ ] Добавить шифрование полезной нагрузки сообщений.
|
||||||
|
|||||||
@@ -25,6 +25,12 @@
|
|||||||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.NNNet">
|
android:theme="@style/Theme.NNNet">
|
||||||
|
<activity
|
||||||
|
android:name=".PacketLogActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".PacketMapActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -85,7 +85,12 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val database = MeshDatabase.getInstance(applicationContext)
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
|
|
||||||
titleText = findViewById(R.id.chatTitleText)
|
titleText = findViewById(R.id.chatTitleText)
|
||||||
subtitleText = findViewById(R.id.chatSubtitleText)
|
subtitleText = findViewById(R.id.chatSubtitleText)
|
||||||
|
|||||||
@@ -94,7 +94,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
val database = MeshDatabase.getInstance(applicationContext)
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
|
|
||||||
deviceCountText = findViewById(R.id.deviceCountText)
|
deviceCountText = findViewById(R.id.deviceCountText)
|
||||||
statusBadge = findViewById(R.id.statusBadge)
|
statusBadge = findViewById(R.id.statusBadge)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package pro.nnnteam.nnnet
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ListView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import pro.nnnteam.nnnet.data.MeshDatabase
|
||||||
|
import pro.nnnteam.nnnet.data.MeshRepository
|
||||||
|
import pro.nnnteam.nnnet.ui.PacketTraceAdapter
|
||||||
|
|
||||||
|
class PacketLogActivity : AppCompatActivity() {
|
||||||
|
private lateinit var repository: MeshRepository
|
||||||
|
private lateinit var adapter: PacketTraceAdapter
|
||||||
|
private val items = mutableListOf<pro.nnnteam.nnnet.data.PacketTraceSummary>()
|
||||||
|
private var currentDirection = MeshRepository.TRACE_OUTGOING
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_packet_log)
|
||||||
|
|
||||||
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
|
||||||
|
adapter = PacketTraceAdapter(this, items)
|
||||||
|
findViewById<ListView>(R.id.packetListView).adapter = adapter
|
||||||
|
|
||||||
|
findViewById<MaterialButton>(R.id.outgoingButton).setOnClickListener {
|
||||||
|
currentDirection = MeshRepository.TRACE_OUTGOING
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
findViewById<MaterialButton>(R.id.incomingButton).setOnClickListener {
|
||||||
|
currentDirection = MeshRepository.TRACE_INCOMING
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
findViewById<MaterialButton>(R.id.relayButton).setOnClickListener {
|
||||||
|
currentDirection = MeshRepository.TRACE_RELAY
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val loaded = repository.recentPacketSummaries(currentDirection)
|
||||||
|
items.clear()
|
||||||
|
items.addAll(loaded)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package pro.nnnteam.nnnet
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import pro.nnnteam.nnnet.data.MeshDatabase
|
||||||
|
import pro.nnnteam.nnnet.data.MeshRepository
|
||||||
|
import pro.nnnteam.nnnet.ui.MapNodeUi
|
||||||
|
import pro.nnnteam.nnnet.ui.NetworkMapView
|
||||||
|
|
||||||
|
class PacketMapActivity : AppCompatActivity() {
|
||||||
|
private lateinit var repository: MeshRepository
|
||||||
|
private lateinit var mapView: NetworkMapView
|
||||||
|
private lateinit var mapMetaText: TextView
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_packet_map)
|
||||||
|
|
||||||
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
|
|
||||||
|
mapView = findViewById(R.id.mapView)
|
||||||
|
mapMetaText = findViewById(R.id.mapMetaText)
|
||||||
|
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
|
||||||
|
|
||||||
|
renderMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderMap() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val local = repository.localProfile()
|
||||||
|
val remoteNodes = repository.mapNodes()
|
||||||
|
val nodes = buildList {
|
||||||
|
add(
|
||||||
|
MapNodeUi(
|
||||||
|
label = local?.displayName() ?: getString(R.string.you_label),
|
||||||
|
peerId = local?.peerId.orEmpty(),
|
||||||
|
isSelf = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remoteNodes
|
||||||
|
.filter { it.peerId != local?.peerId }
|
||||||
|
.take(16)
|
||||||
|
.forEach { profile ->
|
||||||
|
add(
|
||||||
|
MapNodeUi(
|
||||||
|
label = profile.displayName(),
|
||||||
|
peerId = profile.peerId,
|
||||||
|
isSelf = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapView.submitNodes(nodes)
|
||||||
|
mapMetaText.text = getString(R.string.map_mode_hint, nodes.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_settings)
|
setContentView(R.layout.activity_settings)
|
||||||
|
|
||||||
val database = MeshDatabase.getInstance(applicationContext)
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
|
|
||||||
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
|
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
|
||||||
|
|
||||||
@@ -95,6 +100,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
lookupProfile(query)
|
lookupProfile(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
findViewById<MaterialButton>(R.id.openMapButton).setOnClickListener {
|
||||||
|
startActivity(Intent(this, PacketMapActivity::class.java))
|
||||||
|
}
|
||||||
|
findViewById<MaterialButton>(R.id.openPacketLogButton).setOnClickListener {
|
||||||
|
startActivity(Intent(this, PacketLogActivity::class.java))
|
||||||
|
}
|
||||||
findViewById<MaterialButton>(R.id.checkUpdatesButton).setOnClickListener { checkForUpdates() }
|
findViewById<MaterialButton>(R.id.checkUpdatesButton).setOnClickListener { checkForUpdates() }
|
||||||
|
|
||||||
loadLocalProfile()
|
loadLocalProfile()
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import androidx.room.Room
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [MessageEntity::class, OutboundQueueEntity::class, ProfileEntity::class],
|
entities = [MessageEntity::class, OutboundQueueEntity::class, ProfileEntity::class, PacketTraceEntity::class],
|
||||||
version = 2,
|
version = 3,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class MeshDatabase : RoomDatabase() {
|
abstract class MeshDatabase : RoomDatabase() {
|
||||||
abstract fun messageDao(): MessageDao
|
abstract fun messageDao(): MessageDao
|
||||||
abstract fun outboundQueueDao(): OutboundQueueDao
|
abstract fun outboundQueueDao(): OutboundQueueDao
|
||||||
abstract fun profileDao(): ProfileDao
|
abstract fun profileDao(): ProfileDao
|
||||||
|
abstract fun packetTraceDao(): PacketTraceDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import java.util.UUID
|
|||||||
class MeshRepository(
|
class MeshRepository(
|
||||||
private val messageDao: MessageDao,
|
private val messageDao: MessageDao,
|
||||||
private val queueDao: OutboundQueueDao,
|
private val queueDao: OutboundQueueDao,
|
||||||
private val profileDao: ProfileDao
|
private val profileDao: ProfileDao,
|
||||||
|
private val packetTraceDao: PacketTraceDao
|
||||||
) {
|
) {
|
||||||
suspend fun enqueueOutgoingMessage(
|
suspend fun enqueueOutgoingMessage(
|
||||||
senderId: String,
|
senderId: String,
|
||||||
@@ -182,8 +183,70 @@ class MeshRepository(
|
|||||||
|
|
||||||
suspend fun queuedCount(): Int = queueDao.count()
|
suspend fun queuedCount(): Int = queueDao.count()
|
||||||
|
|
||||||
|
suspend fun recordPacketTrace(
|
||||||
|
traceDirection: String,
|
||||||
|
packet: MeshPacket,
|
||||||
|
relatedPeerId: String = "",
|
||||||
|
now: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
packetTraceDao.insert(
|
||||||
|
PacketTraceEntity(
|
||||||
|
traceDirection = traceDirection,
|
||||||
|
packetType = packet.type.name,
|
||||||
|
messageId = packet.messageId,
|
||||||
|
senderId = packet.senderId,
|
||||||
|
targetId = packet.targetId,
|
||||||
|
relatedPeerId = relatedPeerId,
|
||||||
|
payloadPreview = packet.payload.take(120),
|
||||||
|
createdAt = now
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun recentPacketTraces(direction: String, limit: Int = 50): List<PacketTraceEntity> {
|
||||||
|
return packetTraceDao.recentByDirection(direction, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun recentPacketSummaries(direction: String, limit: Int = 50): List<PacketTraceSummary> {
|
||||||
|
return packetTraceDao.recentByDirection(direction, limit).map { trace ->
|
||||||
|
PacketTraceSummary(
|
||||||
|
title = "${directionLabel(trace.traceDirection)} · ${trace.packetType}",
|
||||||
|
subtitle = "${trace.senderId} → ${trace.targetId}",
|
||||||
|
meta = buildString {
|
||||||
|
if (trace.relatedPeerId.isNotBlank()) {
|
||||||
|
append("через ${trace.relatedPeerId} · ")
|
||||||
|
}
|
||||||
|
append(trace.payloadPreview)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun mapNodes(limit: Int = 24): List<ProfileEntity> {
|
||||||
|
val peerIds = packetTraceDao.recentPeerIds(limit)
|
||||||
|
return peerIds.mapNotNull { peerId ->
|
||||||
|
profileDao.findByPeerId(peerId) ?: ProfileEntity(
|
||||||
|
username = peerId.lowercase(),
|
||||||
|
firstName = "",
|
||||||
|
lastName = "",
|
||||||
|
description = "",
|
||||||
|
peerId = peerId,
|
||||||
|
updatedAt = 0L,
|
||||||
|
lastSeenAt = 0L,
|
||||||
|
isLocal = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun normalizeUsername(value: String): String = value.trim().lowercase()
|
private fun normalizeUsername(value: String): String = value.trim().lowercase()
|
||||||
|
|
||||||
|
private fun directionLabel(direction: String): String = when (direction) {
|
||||||
|
TRACE_OUTGOING -> "Исходящий"
|
||||||
|
TRACE_INCOMING -> "Входящий"
|
||||||
|
TRACE_RELAY -> "Транзит"
|
||||||
|
else -> direction
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STATUS_QUEUED = "queued"
|
const val STATUS_QUEUED = "queued"
|
||||||
const val STATUS_SENT = "sent"
|
const val STATUS_SENT = "sent"
|
||||||
@@ -192,6 +255,9 @@ class MeshRepository(
|
|||||||
|
|
||||||
const val DIRECTION_INCOMING = "incoming"
|
const val DIRECTION_INCOMING = "incoming"
|
||||||
const val DIRECTION_OUTGOING = "outgoing"
|
const val DIRECTION_OUTGOING = "outgoing"
|
||||||
|
const val TRACE_OUTGOING = "outgoing"
|
||||||
|
const val TRACE_INCOMING = "incoming"
|
||||||
|
const val TRACE_RELAY = "relay"
|
||||||
|
|
||||||
private const val DEFAULT_MAX_ATTEMPTS = 5
|
private const val DEFAULT_MAX_ATTEMPTS = 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package pro.nnnteam.nnnet.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PacketTraceDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(trace: PacketTraceEntity)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM packet_traces
|
||||||
|
WHERE traceDirection = :direction
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun recentByDirection(direction: String, limit: Int): List<PacketTraceEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM packet_traces
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun recent(limit: Int): List<PacketTraceEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT relatedPeerId FROM packet_traces
|
||||||
|
WHERE relatedPeerId != ''
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun recentPeerIds(limit: Int): List<String>
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package pro.nnnteam.nnnet.data
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "packet_traces")
|
||||||
|
data class PacketTraceEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
|
val traceDirection: String,
|
||||||
|
val packetType: String,
|
||||||
|
val messageId: String,
|
||||||
|
val senderId: String,
|
||||||
|
val targetId: String,
|
||||||
|
val relatedPeerId: String,
|
||||||
|
val payloadPreview: String,
|
||||||
|
val createdAt: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package pro.nnnteam.nnnet.data
|
||||||
|
|
||||||
|
data class PacketTraceSummary(
|
||||||
|
val title: String,
|
||||||
|
val subtitle: String,
|
||||||
|
val meta: String
|
||||||
|
)
|
||||||
@@ -39,6 +39,9 @@ class BleMeshManager(
|
|||||||
private val onAckReceived: (String) -> Unit = {},
|
private val onAckReceived: (String) -> Unit = {},
|
||||||
private val onMessageReceived: (MeshPacket) -> Unit = {},
|
private val onMessageReceived: (MeshPacket) -> Unit = {},
|
||||||
private val onProfileReceived: (MeshPacket) -> Unit = {},
|
private val onProfileReceived: (MeshPacket) -> Unit = {},
|
||||||
|
private val onPacketOutgoing: (MeshPacket, String) -> Unit = { _, _ -> },
|
||||||
|
private val onPacketIncoming: (MeshPacket, String) -> Unit = { _, _ -> },
|
||||||
|
private val onPacketRelay: (MeshPacket, String) -> Unit = { _, _ -> },
|
||||||
private val onError: (String) -> Unit = {},
|
private val onError: (String) -> Unit = {},
|
||||||
private val onLog: (String) -> Unit = {},
|
private val onLog: (String) -> Unit = {},
|
||||||
private val seenPacketCache: SeenPacketCache = SeenPacketCache()
|
private val seenPacketCache: SeenPacketCache = SeenPacketCache()
|
||||||
@@ -251,24 +254,29 @@ class BleMeshManager(
|
|||||||
MeshAction.DropDuplicate -> log("Дубликат пакета отброшен: ${packet.messageId}")
|
MeshAction.DropDuplicate -> log("Дубликат пакета отброшен: ${packet.messageId}")
|
||||||
MeshAction.DropExpired -> log("Просроченный пакет отброшен: ${packet.messageId}")
|
MeshAction.DropExpired -> log("Просроченный пакет отброшен: ${packet.messageId}")
|
||||||
is MeshAction.ConsumeAck -> {
|
is MeshAction.ConsumeAck -> {
|
||||||
|
onPacketIncoming(packet, packet.senderId)
|
||||||
onAckReceived(action.messageId)
|
onAckReceived(action.messageId)
|
||||||
log("ACK обработан: ${action.messageId}")
|
log("ACK обработан: ${action.messageId}")
|
||||||
}
|
}
|
||||||
is MeshAction.ConsumePresence -> {
|
is MeshAction.ConsumePresence -> {
|
||||||
|
onPacketIncoming(packet, packet.senderId)
|
||||||
onPeerDiscovered(action.senderId)
|
onPeerDiscovered(action.senderId)
|
||||||
onStatusChanged("Устройство ${action.senderId} рядом")
|
onStatusChanged("Устройство ${action.senderId} рядом")
|
||||||
log("Сигнал присутствия обработан от ${action.senderId}")
|
log("Сигнал присутствия обработан от ${action.senderId}")
|
||||||
}
|
}
|
||||||
is MeshAction.DeliverMessage -> {
|
is MeshAction.DeliverMessage -> {
|
||||||
|
onPacketIncoming(action.packet, action.packet.senderId)
|
||||||
onMessageReceived(action.packet)
|
onMessageReceived(action.packet)
|
||||||
onStatusChanged("Новое сообщение от ${action.packet.senderId}")
|
onStatusChanged("Новое сообщение от ${action.packet.senderId}")
|
||||||
sendAck(action.packet)
|
sendAck(action.packet)
|
||||||
}
|
}
|
||||||
is MeshAction.CacheProfile -> {
|
is MeshAction.CacheProfile -> {
|
||||||
|
onPacketRelay(action.packet, action.packet.senderId)
|
||||||
onProfileReceived(action.packet)
|
onProfileReceived(action.packet)
|
||||||
broadcastIfAlive(action.packetToRelay)
|
broadcastIfAlive(action.packetToRelay)
|
||||||
}
|
}
|
||||||
is MeshAction.Relay -> {
|
is MeshAction.Relay -> {
|
||||||
|
onPacketRelay(action.packetToRelay, action.packetToRelay.senderId)
|
||||||
log("Ретрансляция пакета ${action.packetToRelay.messageId}")
|
log("Ретрансляция пакета ${action.packetToRelay.messageId}")
|
||||||
broadcastIfAlive(action.packetToRelay)
|
broadcastIfAlive(action.packetToRelay)
|
||||||
}
|
}
|
||||||
@@ -365,6 +373,7 @@ class BleMeshManager(
|
|||||||
type = PacketType.PRESENCE,
|
type = PacketType.PRESENCE,
|
||||||
payload = "presence:$localNodeId"
|
payload = "presence:$localNodeId"
|
||||||
)
|
)
|
||||||
|
onPacketOutgoing(packet, gatt.device.address ?: "")
|
||||||
writePacket(gatt, packet)
|
writePacket(gatt, packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +384,7 @@ class BleMeshManager(
|
|||||||
type = PacketType.ACK,
|
type = PacketType.ACK,
|
||||||
payload = packet.messageId
|
payload = packet.messageId
|
||||||
)
|
)
|
||||||
|
onPacketOutgoing(ack, packet.senderId)
|
||||||
broadcastPacket(ack)
|
broadcastPacket(ack)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +397,7 @@ class BleMeshManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendPacket(packet: MeshPacket): Boolean {
|
fun sendPacket(packet: MeshPacket): Boolean {
|
||||||
|
onPacketOutgoing(packet, packet.targetId)
|
||||||
val directedGatt = activeConnections[packet.targetId]
|
val directedGatt = activeConnections[packet.targetId]
|
||||||
return if (directedGatt != null) {
|
return if (directedGatt != null) {
|
||||||
writePacket(directedGatt, packet)
|
writePacket(directedGatt, packet)
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ class MeshForegroundService : Service() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
val database = MeshDatabase.getInstance(applicationContext)
|
val database = MeshDatabase.getInstance(applicationContext)
|
||||||
repository = MeshRepository(database.messageDao(), database.outboundQueueDao(), database.profileDao())
|
repository = MeshRepository(
|
||||||
|
database.messageDao(),
|
||||||
|
database.outboundQueueDao(),
|
||||||
|
database.profileDao(),
|
||||||
|
database.packetTraceDao()
|
||||||
|
)
|
||||||
bleMeshManager = BleMeshManager(
|
bleMeshManager = BleMeshManager(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
onPeerDiscovered = { address ->
|
onPeerDiscovered = { address ->
|
||||||
@@ -70,6 +75,21 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPacketOutgoing = { packet, relatedPeerId ->
|
||||||
|
serviceScope.launch {
|
||||||
|
repository.recordPacketTrace(MeshRepository.TRACE_OUTGOING, packet, relatedPeerId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPacketIncoming = { packet, relatedPeerId ->
|
||||||
|
serviceScope.launch {
|
||||||
|
repository.recordPacketTrace(MeshRepository.TRACE_INCOMING, packet, relatedPeerId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPacketRelay = { packet, relatedPeerId ->
|
||||||
|
serviceScope.launch {
|
||||||
|
repository.recordPacketTrace(MeshRepository.TRACE_RELAY, packet, relatedPeerId)
|
||||||
|
}
|
||||||
|
},
|
||||||
onError = { message ->
|
onError = { message ->
|
||||||
sendEvent(MeshServiceContract.EVENT_STATUS, "Ошибка: $message")
|
sendEvent(MeshServiceContract.EVENT_STATUS, "Ошибка: $message")
|
||||||
sendEvent(MeshServiceContract.EVENT_LOG, "Ошибка: $message")
|
sendEvent(MeshServiceContract.EVENT_LOG, "Ошибка: $message")
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package pro.nnnteam.nnnet.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
class NetworkMapView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : View(context, attrs) {
|
||||||
|
private val nodePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#4C9EEB")
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
private val selfPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#33A56E")
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#1E2B37")
|
||||||
|
textSize = 34f
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#A7B8C7")
|
||||||
|
strokeWidth = 4f
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nodes: List<MapNodeUi> = emptyList()
|
||||||
|
|
||||||
|
fun submitNodes(items: List<MapNodeUi>) {
|
||||||
|
nodes = items
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (nodes.isEmpty()) return
|
||||||
|
|
||||||
|
val cx = width / 2f
|
||||||
|
val cy = height / 2f
|
||||||
|
val radius = min(width, height) * 0.32f
|
||||||
|
val selfNode = nodes.first()
|
||||||
|
|
||||||
|
canvas.drawCircle(cx, cy, 52f, selfPaint)
|
||||||
|
canvas.drawText(selfNode.label, cx, cy + 86f, textPaint)
|
||||||
|
|
||||||
|
val others = nodes.drop(1)
|
||||||
|
if (others.isEmpty()) return
|
||||||
|
|
||||||
|
others.forEachIndexed { index, node ->
|
||||||
|
val angle = (2 * PI * index / others.size) - PI / 2
|
||||||
|
val nx = cx + (radius * cos(angle)).toFloat()
|
||||||
|
val ny = cy + (radius * sin(angle)).toFloat()
|
||||||
|
canvas.drawLine(cx, cy, nx, ny, linePaint)
|
||||||
|
canvas.drawCircle(nx, ny, 42f, nodePaint)
|
||||||
|
canvas.drawText(node.label, nx, ny + 76f, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MapNodeUi(
|
||||||
|
val label: String,
|
||||||
|
val peerId: String,
|
||||||
|
val isSelf: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package pro.nnnteam.nnnet.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.TextView
|
||||||
|
import pro.nnnteam.nnnet.R
|
||||||
|
import pro.nnnteam.nnnet.data.PacketTraceSummary
|
||||||
|
|
||||||
|
class PacketTraceAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val items: MutableList<PacketTraceSummary>
|
||||||
|
) : BaseAdapter() {
|
||||||
|
private val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
override fun getCount(): Int = items.size
|
||||||
|
|
||||||
|
override fun getItem(position: Int): PacketTraceSummary = items[position]
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long = position.toLong()
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view = convertView ?: inflater.inflate(R.layout.item_packet_trace, parent, false)
|
||||||
|
val item = getItem(position)
|
||||||
|
view.findViewById<TextView>(R.id.traceTitleText).text = item.title
|
||||||
|
view.findViewById<TextView>(R.id.traceSubtitleText).text = item.subtitle
|
||||||
|
view.findViewById<TextView>(R.id.traceMetaText).text = item.meta
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
75
android/app/src/main/res/layout/activity_packet_log.xml
Normal file
75
android/app/src/main/res/layout/activity_packet_log.xml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/screen_background"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/top_bar_background"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="10dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/backButton"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/back"
|
||||||
|
android:src="@android:drawable/ic_media_previous"
|
||||||
|
android:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/packet_log_title"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/outgoingButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/outgoing_packets" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/incomingButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/incoming_packets" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/relayButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/relay_packets" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/packetListView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:divider="@color/chat_divider"
|
||||||
|
android:dividerHeight="1dp"
|
||||||
|
android:listSelector="@android:color/transparent" />
|
||||||
|
</LinearLayout>
|
||||||
50
android/app/src/main/res/layout/activity_packet_map.xml
Normal file
50
android/app/src/main/res/layout/activity_packet_map.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/screen_background"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/top_bar_background"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="10dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/backButton"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/back"
|
||||||
|
android:src="@android:drawable/ic_media_previous"
|
||||||
|
android:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/map_mode_title"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mapMetaText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:textColor="@color/secondary_text" />
|
||||||
|
|
||||||
|
<pro.nnnteam.nnnet.ui.NetworkMapView
|
||||||
|
android:id="@+id/mapView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -180,6 +180,31 @@
|
|||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/diagnostics_title"
|
||||||
|
android:textColor="@color/primary_text"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/openMapButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/map_mode_title"
|
||||||
|
app:cornerRadius="18dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/openPacketLogButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/packet_log_title"
|
||||||
|
app:cornerRadius="18dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/versionText"
|
android:id="@+id/versionText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
31
android/app/src/main/res/layout/item_packet_trace.xml
Normal file
31
android/app/src/main/res/layout/item_packet_trace.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/traceTitleText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primary_text"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/traceSubtitleText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/secondary_text"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/traceMetaText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/accent_blue"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -51,4 +51,12 @@
|
|||||||
<string name="no_profile_description">Описание не указано</string>
|
<string name="no_profile_description">Описание не указано</string>
|
||||||
<string name="peer_id_unknown">peerId пока неизвестен</string>
|
<string name="peer_id_unknown">peerId пока неизвестен</string>
|
||||||
<string name="peer_id_value">peerId: %1$s</string>
|
<string name="peer_id_value">peerId: %1$s</string>
|
||||||
|
<string name="diagnostics_title">Диагностика сети</string>
|
||||||
|
<string name="map_mode_title">Режим карты</string>
|
||||||
|
<string name="packet_log_title">Журнал пакетов</string>
|
||||||
|
<string name="outgoing_packets">Исходящие</string>
|
||||||
|
<string name="incoming_packets">Входящие</string>
|
||||||
|
<string name="relay_packets">Транзит</string>
|
||||||
|
<string name="you_label">Вы</string>
|
||||||
|
<string name="map_mode_hint">Показано устройств: %1$d. Карта строится по сетевым связям и не является GPS-позицией.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- Mesh Layer: маршрутизация, TTL, дедупликация, ACK, ретрансляция профильных пакетов.
|
- Mesh Layer: маршрутизация, TTL, дедупликация, ACK, ретрансляция профильных пакетов.
|
||||||
- Messaging Layer: список чатов, отдельный экран диалога, статусы доставки, история.
|
- Messaging Layer: список чатов, отдельный экран диалога, статусы доставки, история.
|
||||||
- Storage Layer: Room для локального хранения сообщений, очереди и профилей.
|
- Storage Layer: Room для локального хранения сообщений, очереди и профилей.
|
||||||
|
- Diagnostics Layer: карта сети и журнал пакетов, построенные на данных `Room`.
|
||||||
- Delivery Layer: retry queue, ACK timeout, повторные отправки из фонового сервиса.
|
- Delivery Layer: retry queue, ACK timeout, повторные отправки из фонового сервиса.
|
||||||
- Update Layer: `version.json`, changelog и ручная/автоматическая проверка обновлений клиента.
|
- Update Layer: `version.json`, changelog и ручная/автоматическая проверка обновлений клиента.
|
||||||
- Profile Layer: локальный профиль пользователя, `username` как основной идентификатор, кэш профилей из mesh-сети и разрешение `username -> peerId`.
|
- Profile Layer: локальный профиль пользователя, `username` как основной идентификатор, кэш профилей из mesh-сети и разрешение `username -> peerId`.
|
||||||
@@ -16,12 +17,14 @@
|
|||||||
- Настройки вынесены в меню `три точки`, отдельный debug-лог из пользовательского интерфейса убран.
|
- Настройки вынесены в меню `три точки`, отдельный debug-лог из пользовательского интерфейса убран.
|
||||||
- Отправка сообщений доступна только из экрана конкретного диалога.
|
- Отправка сообщений доступна только из экрана конкретного диалога.
|
||||||
- В настройках пользователь редактирует свой профиль и ищет другие профили по `username`.
|
- В настройках пользователь редактирует свой профиль и ищет другие профили по `username`.
|
||||||
|
- В настройках доступны режим карты сети и экран журнала пакетов.
|
||||||
|
|
||||||
## Топология сети
|
## Топология сети
|
||||||
- Выделенный сервер или хост для работы mesh не нужен.
|
- Выделенный сервер или хост для работы mesh не нужен.
|
||||||
- Все узлы равноправны: каждый телефон может быть источником, получателем и ретранслятором.
|
- Все узлы равноправны: каждый телефон может быть источником, получателем и ретранслятором.
|
||||||
- Сеть не рассчитана на бесконечное число пользователей. Масштаб ограничивается радиусом BLE, количеством соседних соединений, частотой ретрансляции и ограничениями Android по энергии и фону.
|
- Сеть не рассчитана на бесконечное число пользователей. Масштаб ограничивается радиусом BLE, количеством соседних соединений, частотой ретрансляции и ограничениями Android по энергии и фону.
|
||||||
- Профильные данные передаются отдельными mesh-пакетами и кэшируются на устройствах. Это даёт распределённый каталог пользователей без центрального сервера, но актуальность данных зависит от распространения пакетов по сети.
|
- Профильные данные передаются отдельными mesh-пакетами и кэшируются на устройствах. Это даёт распределённый каталог пользователей без центрального сервера, но актуальность данных зависит от распространения пакетов по сети.
|
||||||
|
- Карта сети строится как относительная топология связей, а не как GPS/геометрическая карта здания. Высота этажей пока не моделируется.
|
||||||
|
|
||||||
## Сетевой пакет (черновик)
|
## Сетевой пакет (черновик)
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-broadcast-pin fs-2"></i><h5 class="mt-2">BLE-поиск</h5><p class="mb-0">Обнаружение ближайших узлов и обмен пакетами без интернета.</p></div></div>
|
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-broadcast-pin fs-2"></i><h5 class="mt-2">BLE-поиск</h5><p class="mb-0">Обнаружение ближайших узлов и обмен пакетами без интернета.</p></div></div>
|
||||||
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-share fs-2"></i><h5 class="mt-2">Mesh-ретрансляция</h5><p class="mb-0">Передача сообщений hop-by-hop с TTL, ACK и повторными попытками.</p></div></div>
|
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-share fs-2"></i><h5 class="mt-2">Mesh-ретрансляция</h5><p class="mb-0">Передача сообщений hop-by-hop с TTL, ACK и повторными попытками.</p></div></div>
|
||||||
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-database fs-2"></i><h5 class="mt-2">Локальное хранение</h5><p class="mb-0">Room хранит историю сообщений, очередь доставки и кэш профилей пользователей.</p></div></div>
|
<div class="col-md-4"><div class="feature p-3 rounded-4"><i class="bi bi-database fs-2"></i><h5 class="mt-2">Локальное хранение</h5><p class="mb-0">Room хранит историю сообщений, очередь доставки, кэш профилей и журнал пакетов.</p></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user