message status updates are coded but not yet tested.

This commit is contained in:
geeksville 2020-05-30 19:58:36 -07:00
parent b2d8b30d5b
commit 7506d712ff
9 changed files with 157 additions and 130 deletions

View file

@ -24,11 +24,33 @@ data class DataPacket(
val bytes: ByteArray?,
val dataType: Int, // A value such as MeshProtos.Data.Type.OPAQUE_VALUE
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
var rxTime: Long = System.currentTimeMillis(), // msecs since 1970
var time: Long = System.currentTimeMillis(), // msecs since 1970
var id: Int = 0, // 0 means unassigned
var status: MessageStatus? = MessageStatus.UNKNOWN
) : Parcelable {
/**
* If there was an error with this message, this string describes what was wrong.
*/
var errorMessage: String? = null
/**
* Syntactic sugar to make it easy to create text messages
*/
constructor(to: String? = ID_BROADCAST, text: String) : this(
to, text.toByteArray(utf8),
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
)
/**
* If this is a text message, return the string, otherwise null
*/
val text: String?
get() = if (dataType == MeshProtos.Data.Type.CLEAR_TEXT_VALUE)
bytes?.toString(utf8)
else
null
// Autogenerated comparision, because we have a byte array
constructor(parcel: Parcel) : this(
@ -50,7 +72,7 @@ data class DataPacket(
if (from != other.from) return false
if (to != other.to) return false
if (rxTime != other.rxTime) return false
if (time != other.time) return false
if (id != other.id) return false
if (dataType != other.dataType) return false
if (!bytes!!.contentEquals(other.bytes!!)) return false
@ -62,7 +84,7 @@ data class DataPacket(
override fun hashCode(): Int {
var result = from.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + rxTime.hashCode()
result = 31 * result + time.hashCode()
result = 31 * result + id
result = 31 * result + dataType
result = 31 * result + bytes!!.contentHashCode()
@ -75,7 +97,7 @@ data class DataPacket(
parcel.writeByteArray(bytes)
parcel.writeInt(dataType)
parcel.writeString(from)
parcel.writeLong(rxTime)
parcel.writeLong(time)
parcel.writeInt(id)
parcel.writeParcelable(status, flags)
}
@ -90,7 +112,7 @@ data class DataPacket(
parcel.createByteArray()
parcel.readInt()
from = parcel.readString()
rxTime = parcel.readLong()
time = parcel.readLong()
id = parcel.readInt()
status = parcel.readParcelable(MessageStatus::class.java.classLoader)
}

View file

@ -31,7 +31,6 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.TextMessage
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.*
import com.geeksville.mesh.ui.*
@ -398,7 +397,7 @@ class MainActivity : AppCompatActivity(), Logging,
else -> R.drawable.cloud_off
}
connectStatusImage.setImageDrawable(getDrawable(image))
connectStatusImage.setImageResource(image)
})
askToRate()
@ -623,6 +622,15 @@ class MainActivity : AppCompatActivity(), Logging,
errormsg("Unhandled dataType ${payload.dataType}")
}
}
MeshService.ACTION_MESSAGE_STATUS -> {
debug("received message status from service")
val id = intent.getIntExtra(EXTRA_PACKET_ID, 0)
val status = intent.getParcelableExtra<MessageStatus>(EXTRA_STATUS)
model.messagesState.updateStatus(id, status)
}
MeshService.ACTION_MESH_CONNECTED -> {
val connected =
MeshService.ConnectionState.valueOf(
@ -650,10 +658,7 @@ class MainActivity : AppCompatActivity(), Logging,
registerMeshReceiver()
// Init our messages table with the service's record of past text messages
model.messagesState.messages.value = service.oldMessages.map {
TextMessage(it)
}
model.messagesState.messages.value = service.oldMessages
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())

View file

@ -16,7 +16,8 @@ data class MyNodeInfo(
val shouldUpdate: Boolean, // this device has old firmware
val currentPacketId: Long,
val nodeNumBits: Int,
val packetIdBits: Int
val packetIdBits: Int,
val messageTimeoutMsec: Int
) : Parcelable {
/** A human readable description of the software/hardware version */
val firmwareString: String get() = "$model $region/$firmwareVersion"
@ -31,6 +32,7 @@ data class MyNodeInfo(
parcel.readByte() != 0.toByte(),
parcel.readLong(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt()
) {
}
@ -46,6 +48,7 @@ data class MyNodeInfo(
parcel.writeLong(currentPacketId)
parcel.writeInt(nodeNumBits)
parcel.writeInt(packetIdBits)
parcel.writeInt(messageTimeoutMsec)
}
override fun describeContents(): Int {
@ -61,4 +64,6 @@ data class MyNodeInfo(
return arrayOfNulls(size)
}
}
}

View file

@ -6,36 +6,17 @@ import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.utf8
import java.util.*
/**
* the model object for a text message
*
* if errorMessage is set then we had a problem sending this message
*/
data class TextMessage(
val from: String,
val text: String,
val date: Date = Date(),
val errorMessage: String? = null
) {
/// We can auto init from data packets
constructor(payload: DataPacket) : this(
payload.from!!,
payload.bytes!!.toString(utf8),
date = Date(payload.rxTime)
)
}
class MessagesState(private val ui: UIViewModel) : Logging {
private val testTexts = listOf(
TextMessage(
DataPacket(
"+16508765310",
"I found the cache"
),
TextMessage(
DataPacket(
"+16508765311",
"Help! I've fallen and I can't get up."
)
@ -44,44 +25,47 @@ class MessagesState(private val ui: UIViewModel) : Logging {
// If the following (unused otherwise) line is commented out, the IDE preview window works.
// if left in the preview always renders as empty.
val messages =
object : MutableLiveData<List<TextMessage>>(if (isEmulator) testTexts else listOf()) {
object : MutableLiveData<List<DataPacket>>(if (isEmulator) testTexts else listOf()) {
}
/// add a message our GUI list of past msgs
private fun addMessage(m: TextMessage) {
fun addMessage(m: DataPacket) {
// FIXME - don't just slam in a new list each time, it probably causes extra drawing.
messages.value = messages.value!! + m
}
/// Add a message that was encapsulated in a data packet
fun addMessage(payload: DataPacket) = addMessage(TextMessage(payload))
fun updateStatus(id: Int, status: MessageStatus) {
// Super inefficent but this is rare
val msgs = messages.value!!
msgs.find { it.id == id }?.let { p ->
if (p.status != status) {
p.status = status
// Trigger an expensive complete redraw FIXME
messages.value = msgs
}
}
}
/// Send a message and added it to our GUI log
fun sendMessage(str: String, dest: String = DataPacket.ID_BROADCAST) {
var error: String? = null
val service = ui.meshService
val p = DataPacket(
dest,
str.toByteArray(utf8),
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
)
if (service != null)
try {
service.send(
DataPacket(
dest,
str.toByteArray(utf8),
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
)
)
service.send(p)
} catch (ex: RemoteException) {
error = "Error: ${ex.message}"
p.errorMessage = "Error: ${ex.message}"
}
else
error = "Error: No Mesh service"
p.errorMessage = "Error: No Mesh service"
addMessage(
TextMessage(
ui.nodeDB.myId.value!!,
str,
errorMessage = error
)
)
addMessage(p)
}
}

View file

@ -15,3 +15,5 @@ const val EXTRA_PERMANENT = "$prefix.Permanent"
const val EXTRA_PAYLOAD = "$prefix.Payload"
const val EXTRA_NODEINFO = "$prefix.NodeInfo"
const val EXTRA_PACKET_ID = "$prefix.PacketId"
const val EXTRA_STATUS = "$prefix.Status"

View file

@ -9,6 +9,7 @@ import android.content.IntentFilter
import android.graphics.Color
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
import android.os.RemoteException
import android.widget.Toast
import androidx.annotation.RequiresApi
@ -52,6 +53,7 @@ class MeshService : Service(), Logging {
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA"
const val ACTION_NODE_CHANGE = "$prefix.NODE_CHANGE"
const val ACTION_MESH_CONNECTED = "$prefix.MESH_CONNECTED"
const val ACTION_MESSAGE_STATUS = "$prefix.MESSAGE_STATUS"
class IdNotFoundException(id: String) : Exception("ID not found $id")
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
@ -292,6 +294,19 @@ class MeshService : Service(), Logging {
explicitBroadcast(intent)
}
private fun broadcastMessageStatus(p: DataPacket) {
if (p.id == 0) {
debug("Ignoring anonymous packet status")
} else {
debug("Broadcasting message status $p")
val intent = Intent(ACTION_MESSAGE_STATUS)
intent.putExtra(EXTRA_PACKET_ID, p.id)
intent.putExtra(EXTRA_STATUS, p.status as Parcelable)
explicitBroadcast(intent)
}
}
/// Safely access the radio service, if not connected an exception will be thrown
private val connectedRadio: IRadioInterfaceService
get() = (if (connectionState == ConnectionState.CONNECTED) radio.serviceP else null)
@ -703,7 +718,7 @@ class MeshService : Service(), Logging {
DataPacket(
from = fromId,
to = toId,
rxTime = rxTime * 1000L,
time = rxTime * 1000L,
id = packet.id,
dataType = data.typValue,
bytes = bytes
@ -800,6 +815,9 @@ class MeshService : Service(), Logging {
/// If apps try to send packets when our radio is sleeping, we queue them here instead
private val offlineSentPackets = mutableListOf<DataPacket>()
/** Keep a record of recently sent packets, so we can properly handle ack/nak */
private val sentPackets = mutableMapOf<Int, DataPacket>()
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedMeshPacket(packet: MeshPacket) {
if (haveNodeDB) {
@ -820,12 +838,25 @@ class MeshService : Service(), Logging {
// encapsulate our payload in the proper protobufs and fire it off
val packet = toMeshPacket(p)
p.status = MessageStatus.ENROUTE
p.time =
System.currentTimeMillis() // update time to the actual time we started sending
sendToRadio(packet)
broadcastMessageStatus(p)
}
offlineSentPackets.clear()
}
/**
* Handle an ack/nak packet by updating sent message status
*/
private fun handleAckNak(isAck: Boolean, id: Int) {
sentPackets.remove(id)?.let { p ->
p.status = if (isAck) MessageStatus.DELIVERED else MessageStatus.ERROR
broadcastMessageStatus(p)
}
}
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun processReceivedMeshPacket(packet: MeshPacket) {
val fromNum = packet.from
@ -859,6 +890,12 @@ class MeshService : Service(), Logging {
if (p.hasUser())
handleReceivedUser(fromNum, p.user)
if (p.successId != 0)
handleAckNak(true, p.successId)
if (p.failId != 0)
handleAckNak(false, p.failId)
}
private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt()
@ -1169,8 +1206,9 @@ class MeshService : Service(), Logging {
firmwareUpdateFilename != null,
SoftwareUpdateService.shouldUpdate(this@MeshService, firmwareVersion),
currentPacketId.toLong() and 0xffffffffL,
nodeNumBits,
packetIdBits
if (nodeNumBits == 0) 8 else nodeNumBits,
if (packetIdBits == 0) 8 else packetIdBits,
if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec // constants from current device code
)
}
@ -1377,7 +1415,22 @@ class MeshService : Service(), Logging {
}
}
private
/**
* Remove any sent packets that have been sitting around too long
*
* Note: we give each message what the timeout the device code is using, though in the normal
* case the device will fail after 3 retries much sooner than that (and it will provide a nak to us)
*/
private fun deleteOldPackets() {
myNodeInfo?.apply {
val now = System.currentTimeMillis()
sentPackets.values.forEach { p ->
if (p.status == MessageStatus.ENROUTE && p.time + messageTimeoutMsec < now)
handleAckNak(false, p.id)
}
}
}
val binder = object : IMeshService.Stub() {
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
@ -1416,8 +1469,6 @@ class MeshService : Service(), Logging {
p: DataPacket
) {
toRemoteExceptions {
info("sendData dest=${p.to} <- ${p.bytes!!.size} bytes (connectionState=$connectionState)")
// Init from and id
myNodeID?.let { myId ->
if (p.from == DataPacket.ID_LOCAL)
@ -1427,10 +1478,16 @@ class MeshService : Service(), Logging {
p.id = generatePacketId()
}
info("sendData dest=${p.to}, id=${p.id} <- ${p.bytes!!.size} bytes (connectionState=$connectionState)")
// Keep a record of datapackets, so GUIs can show proper chat history
rememberDataPacket(p)
if (p.id != 0) { // If we have an ID we can wait for an ack or nak
deleteOldPackets()
sentPackets[p.id] = p
}
// If radio is sleeping, queue the packet
when (connectionState) {
ConnectionState.DEVICE_SLEEP -> {

View file

@ -4,67 +4,3 @@ import com.geeksville.android.Logging
object UILog : Logging
/*
val palette = lightColorPalette() // darkColorPalette()
@Composable
fun MeshApp() {
val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
MaterialTheme(colors = palette) {
ModalDrawerLayout(
drawerState = drawerState,
onStateChange = onDrawerStateChange,
gesturesEnabled = drawerState == DrawerState.Opened,
drawerContent = {
AppDrawer(
currentScreen = AppStatus.currentScreen,
closeDrawer = { onDrawerStateChange(DrawerState.Closed) }
)
}, bodyContent = { AppContent { onDrawerStateChange(DrawerState.Opened) } })
}
}
@Preview
@Composable
fun previewView() {
// It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
UsersContent()
}
}
@Composable
private fun AppContent(openDrawer: () -> Unit) {
// crossfade breaks onCommit behavior because it keeps old views around
//Crossfade(AppStatus.currentScreen) { screen ->
//Surface(color = (MaterialTheme.colors()).background) {
Scaffold(topAppBar = {
TopAppBar(
title = { Text(text = "Meshtastic") },
navigationIcon = {
//Container(LayoutSize(40.dp, 40.dp)) {
VectorImageButton(R.drawable.ic_launcher_new_foreground) {
openDrawer()
}
//}
}
)
}) {
when (AppStatus.currentScreen) {
Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> UsersContent()
Screen.channel -> ChannelContent(UIState.getChannel())
Screen.map -> MapContent()
else -> TODO()
}
}
//}
}
*/

View file

@ -13,14 +13,16 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
import com.geeksville.mesh.model.TextMessage
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.adapter_message_layout.view.*
import kotlinx.android.synthetic.main.messages_fragment.*
import java.text.SimpleDateFormat
import java.util.*
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
fun EditText.on(actionId: Int, func: () -> Unit) {
@ -129,13 +131,27 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
holder.messageText.text = msg.text
}
holder.messageTime.text = dateFormat.format(msg.date)
holder.messageTime.text = dateFormat.format(Date(msg.time))
val icon = when (msg.status) {
MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24
MessageStatus.DELIVERED -> R.drawable.cloud_on
MessageStatus.ENROUTE -> R.drawable.ic_twotone_cloud_24
MessageStatus.ERROR -> R.drawable.cloud_off
else -> null
}
if (icon != null) {
holder.messageStatusIcon.setImageResource(icon)
holder.messageStatusIcon.visibility = View.VISIBLE
} else
holder.messageStatusIcon.visibility = View.INVISIBLE
}
private var messages = arrayOf<TextMessage>()
private var messages = arrayOf<DataPacket>()
/// Called when our node DB changes
fun onMessagesChanged(nodesIn: Collection<TextMessage>) {
fun onMessagesChanged(nodesIn: Collection<DataPacket>) {
messages = nodesIn.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages

@ -1 +1 @@
Subproject commit d624f8cb17a2f723a51649d442791b195a18604a
Subproject commit e9c7f9b95d490aea3f0f213d4666d2dbf7e2111c