Make UART working

This commit is contained in:
Sylwester Zieliński
2022-02-23 10:52:21 +01:00
parent ded529410f
commit 2fe8edca70
30 changed files with 513 additions and 333 deletions

View File

@@ -14,6 +14,7 @@
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/AppTheme.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -2,13 +2,7 @@ package no.nordicsemi.android.theme.view.dialog
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -22,7 +16,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.theme.R
@@ -51,10 +44,12 @@ fun StringListView(config: StringListDialogConfig) {
) {
Text(
text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString(),
fontSize = 20.sp
style = MaterialTheme.typography.headlineMedium
)
}
Spacer(modifier = Modifier.size(8.dp))
Column(
modifier = Modifier
.fillMaxHeight(0.8f)
@@ -62,13 +57,13 @@ fun StringListView(config: StringListDialogConfig) {
) {
config.items.forEachIndexed { i, entry ->
Column(
Row(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.clickable { config.onResult(ItemSelectedResult(i)) }
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row {
config.leftIcon?.let {
Image(
modifier = Modifier.padding(horizontal = 4.dp),
@@ -78,14 +73,11 @@ fun StringListView(config: StringListDialogConfig) {
}
Text(
text = entry,
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.titleLarge
)
}
}
}
}
Column(

View File

@@ -1,6 +1,6 @@
package no.nordicsemi.android.uart.data
enum class MacroEol(val eolIndex: Int) {
enum class MacroEol(val index: Int) {
LF(0),
CR(1),
CR_LF(2);

View File

@@ -1,6 +1,6 @@
package no.nordicsemi.android.uart.data
enum class MacroIcon(val index: Int) {
enum class MacroIcon(public val index: Int) {
LEFT(0),
UP(1),
RIGHT(2),

View File

@@ -1,10 +1,9 @@
package no.nordicsemi.android.uart.data
import no.nordicsemi.android.uart.db.XmlCommand
private const val MACROS_SIZES = 9
data class UARTConfiguration(
val id: Int?,
val name: String,
val macros: List<UARTMacro?> = List<UARTMacro?>(9) { null }
) {

View File

@@ -1,8 +1,14 @@
package no.nordicsemi.android.uart.data
import no.nordicsemi.android.utils.EMPTY
internal data class UARTData(
val text: String = String.EMPTY,
val messages: List<UARTOutputRecord> = emptyList(),
val batteryLevel: Int? = null,
) {
val displayMessages = messages.reversed().take(10)
}
internal data class UARTOutputRecord(
val text: String,
val timestamp: Long = System.currentTimeMillis()
)

View File

@@ -1,3 +1,3 @@
package no.nordicsemi.android.uart.data
data class UARTMacro(val icon: MacroIcon, val command: String, val newLineChar: MacroEol)
data class UARTMacro(val icon: MacroIcon, val command: String?, val newLineChar: MacroEol)

View File

@@ -75,10 +75,10 @@ internal class UARTManager(
override fun initialize() {
setNotificationCallback(txCharacteristic).asFlow().onEach {
val text: String = it.getStringValue(0) ?: String.EMPTY
data.value = data.value.copy(text = text)
}
data.value = data.value.copy(messages = data.value.messages + UARTOutputRecord(text))
}.launchIn(scope)
requestMtu(260).enqueue()
requestMtu(517).enqueue()
enableNotifications(txCharacteristic).enqueue()
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>().onEach {
@@ -88,7 +88,7 @@ internal class UARTManager(
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service: BluetoothGattService = gatt.getService(UART_SERVICE_UUID)
val service: BluetoothGattService? = gatt.getService(UART_SERVICE_UUID)
if (service != null) {
rxCharacteristic = service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID)
txCharacteristic = service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID)
@@ -99,16 +99,13 @@ internal class UARTManager(
rxCharacteristic?.let {
val rxProperties: Int = it.properties
writeRequest = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
writeCommand =
rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
writeCommand = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
// Set the WRITE REQUEST type when the characteristic supports it.
// This will allow to send long write (also if the characteristic support it).
// In case there is no WRITE REQUEST property, this manager will divide texts
// longer then MTU-3 bytes into up to MTU-3 bytes chunks.
if (writeRequest) {
it.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
} else {
if (!writeRequest) {
useLongWrite = false
}
}
@@ -130,7 +127,12 @@ internal class UARTManager(
if (rxCharacteristic == null) return
if (!TextUtils.isEmpty(text)) {
scope.launchWithCatch {
val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
val writeType = if (useLongWrite) {
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
} else {
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
}
val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), writeType)
if (!useLongWrite) {
request.split()
}
@@ -139,6 +141,10 @@ internal class UARTManager(
}
}
fun clearItems() {
data.value = data.value.copy(messages = emptyList())
}
override fun getGattCallback(): BleManagerGattCallback {
return UARTManagerGattCallback()
}

View File

@@ -1,6 +1,5 @@
package no.nordicsemi.android.uart.data
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import no.nordicsemi.android.uart.db.*
@@ -26,17 +25,17 @@ internal class UARTPersistentDataSource @Inject constructor(
val serializer: Serializer = Persister(format)
val configuration = serializer.read(XmlConfiguration::class.java, xml)
UARTConfiguration(configuration.name ?: "Unknown", createMacro(configuration.commands))
UARTConfiguration(it._id, configuration.name ?: "Unknown", createMacro(configuration.commands))
}
}
private fun createMacro(macros: Array<XmlCommand?>): List<UARTMacro?> {
private fun createMacro(macros: Array<XmlMacro?>): List<UARTMacro?> {
return macros.map {
if (it == null) {
null
} else {
val icon = MacroIcon.create(it.iconIndex)
it.command?.let { c -> UARTMacro(icon, c, it.eol) }
UARTMacro(icon, it.command, it.eol)
}
}
}
@@ -46,13 +45,29 @@ internal class UARTPersistentDataSource @Inject constructor(
val strategy: Strategy = VisitorStrategy(CommentVisitor())
val serializer: Serializer = Persister(strategy, format)
val writer = StringWriter()
serializer.write(configuration, writer)
serializer.write(configuration.toXmlConfiguration(), writer)
val xml = writer.toString()
configurationsDao.insert(Configuration(0, configuration.name, xml, 0))
configurationsDao.insert(Configuration(configuration.id, configuration.name, xml, 0))
}
suspend fun deleteConfiguration(configuration: UARTConfiguration) {
configurationsDao.delete(configuration.name)
}
private fun UARTConfiguration.toXmlConfiguration(): XmlConfiguration {
val xmlConfiguration = XmlConfiguration()
xmlConfiguration.name = name
val commands = macros.map { macro ->
macro?.let {
XmlMacro().apply {
setEol(it.newLineChar.index)
command = it.command
iconIndex = it.icon.index
}
}
}.toTypedArray()
xmlConfiguration.commands = commands
return xmlConfiguration
}
}

View File

@@ -16,7 +16,7 @@ internal class CommentVisitor : Visitor {
}
override fun write(type: Type, node: NodeMap<OutputNode>) {
if (type.type == Array<XmlCommand>::class.java) {
if (type.type == Array<XmlMacro>::class.java) {
val element = node.node
val builder =
StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:")

View File

@@ -1,85 +0,0 @@
/*
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.uart.db
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.MacroIcon
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.Root
import org.simpleframework.xml.Text
@Root
internal class XmlCommand {
/**
* Returns the command that will be sent to UART device.
* @return the command
*/
/**
* Sets the command.
* @param command the command that will be sent to UART device
*/
@Text(required = false)
var command: String? = null
/**
* Returns whether the icon is active.
* @return true if it's active
*/
/**
* Sets whether the command is active.
* @param active true to make it active
*/
@Attribute(required = false)
var isActive = false
/**
* Returns the new line type.
* @return end of line terminator
*/
@Attribute(required = false)
var eol = MacroEol.LF
private set
@Attribute(required = false)
private var icon = MacroIcon.LEFT
/**
* Sets the new line type.
* @param eol end of line terminator
*/
fun setEol(eol: Int) {
this.eol = MacroEol.values()[eol]
}
/**
* Returns the icon index.
* @return the icon index
*/
/**
* Sets the icon index.
* @param index index of the icon.
*/
var iconIndex: Int
get() = icon.index
set(index) {
icon = MacroIcon.values()[index]
}
}

View File

@@ -19,30 +19,57 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.uart.db
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementArray
import org.simpleframework.xml.Root
import org.simpleframework.xml.core.PersistenceException
import org.simpleframework.xml.core.Validate
package no.nordicsemi.android.uart.db;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.ElementArray;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.core.PersistenceException;
import org.simpleframework.xml.core.Validate;
@Root
internal class XmlConfiguration @JvmOverloads constructor(
@field:Attribute(required = false, empty = "Unnamed")
var name: String? = "",
public class XmlConfiguration {
public static final int COMMANDS_COUNT = 9;
@field:ElementArray
var commands: Array<XmlCommand?> = arrayOfNulls(COMMANDS_COUNT)
) {
@Attribute(required = false, empty = "Unnamed")
private String name;
@ElementArray
private XmlMacro[] commands = new XmlMacro[COMMANDS_COUNT];
/**
* Returns the field name
*
* @return optional name
*/
public String getName() {
return name;
}
/**
* Sets the name to specified value
* @param name the new name
*/
public void setName(final String name) {
this.name = name;
}
/**
* Returns the array of commands. There is always 9 of them.
* @return the commands array
*/
public XmlMacro[] getCommands() {
return commands;
}
public void setCommands(XmlMacro[] commands) {
this.commands = commands;
}
@Validate
@Throws(PersistenceException::class)
private fun validate() {
if (commands.size != COMMANDS_COUNT) throw PersistenceException("There must be always $COMMANDS_COUNT commands in a configuration.")
}
companion object {
const val COMMANDS_COUNT = 9
private void validate() throws PersistenceException{
if (commands == null || commands.length != COMMANDS_COUNT)
throw new PersistenceException("There must be always " + COMMANDS_COUNT + " commands in a configuration.");
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.uart.db;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Text;
import no.nordicsemi.android.uart.data.MacroEol;
import no.nordicsemi.android.uart.data.MacroIcon;
@Root
public class XmlMacro {
@Text(required = false)
private String command;
@Attribute(required = false)
private boolean active = false;
@Attribute(required = false)
private MacroEol eol = MacroEol.LF;
@Attribute(required = false)
private MacroIcon icon = MacroIcon.LEFT;
/**
* Sets the command.
* @param command the command that will be sent to UART device
*/
public void setCommand(final String command) {
this.command = command;
}
/**
* Sets whether the command is active.
* @param active true to make it active
*/
public void setActive(final boolean active) {
this.active = active;
}
/**
* Sets the new line type.
* @param eol end of line terminator
*/
public void setEol(final int eol) {
this.eol = MacroEol.values()[eol];
}
/**
* Sets the icon index.
* @param index index of the icon.
*/
public void setIconIndex(final int index) {
this.icon = MacroIcon.values()[index];
}
/**
* Returns the command that will be sent to UART device.
* @return the command
*/
public String getCommand() {
return command;
}
/**
* Returns whether the icon is active.
* @return true if it's active
*/
public boolean isActive() {
return active;
}
/**
* Returns the new line type.
* @return end of line terminator
*/
public MacroEol getEol() {
return eol;
}
/**
* Returns the icon index.
* @return the icon index
*/
public int getIconIndex() {
return icon.getIndex();
}
/**
* Returns the EOL index.
* @return the EOL index
*/
public int getEolIndex() {
return eol.getIndex();
}
}

View File

@@ -48,7 +48,13 @@ class UARTRepository @Inject constructor(
}
fun runMacro(macro: UARTMacro) {
manager?.send(macro.command.parseWithNewLineChar(macro.newLineChar))
macro.command?.parseWithNewLineChar(macro.newLineChar)?.let {
manager?.send(it)
}
}
fun clearItems() {
manager?.clearItems()
}
private suspend fun UARTManager.start(device: BluetoothDevice) {

View File

@@ -1,35 +1,55 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.utils.EMPTY
@Composable
internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit) {
val name = remember { mutableStateOf(String.EMPTY) }
val isError = remember { mutableStateOf(false) }
internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
val name = rememberSaveable { mutableStateOf(String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) }
Dialog(onDismissRequest = { onDismiss() }) {
Surface(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp),
shadowElevation = 2.dp,
) {
Column(verticalArrangement = Arrangement.SpaceBetween) {
Text(
text = stringResource(id = R.string.uart_configuration_dialog_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
// Spacer(modifier = Modifier.height(16.dp))
Column {
NameInput(name, isError)
Spacer(modifier = Modifier.height(16.dp))
// Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onEvent(OnEditFinish) }) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
@@ -37,7 +57,7 @@ internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit) {
TextButton(onClick = {
if (isNameValid(name.value)) {
onEvent(OnEditFinish)
onDismiss()
onEvent(OnAddConfiguration(name.value))
} else {
isError.value = true
@@ -47,7 +67,8 @@ internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit) {
}
}
}
}
}
}
@Composable
@@ -55,22 +76,26 @@ private fun NameInput(
name: MutableState<String>,
isError: MutableState<Boolean>
) {
Column {
Column(modifier = Modifier.padding(16.dp)) {
TextField(
text = name.value,
hint = stringResource(id = R.string.uart_macro_dialog_command)
hint = stringResource(id = R.string.uart_configuration_hint)
) {
isError.value = false
name.value = it
}
if (isError.value) {
val errorText = if (isError.value) {
stringResource(id = R.string.uart_name_empty)
} else {
String.EMPTY
}
Text(
text = stringResource(id = R.string.uart_name_empty),
text = errorText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp))
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -38,11 +39,11 @@ private const val GRID_SIZE = 5
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun UARTAddMacroDialog(onEvent: (UARTViewEvent) -> Unit) {
val newLineChar = remember { mutableStateOf(MacroEol.LF) }
val command = remember { mutableStateOf(String.EMPTY) }
val isError = remember { mutableStateOf(false) }
val selectedIcon = remember { mutableStateOf(MacroIcon.values()[0]) }
internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> Unit) {
val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) }
val command = rememberSaveable { mutableStateOf(macro?.command ?: String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) }
val selectedIcon = rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.values()[0]) }
Dialog(onDismissRequest = { onEvent(OnEditFinish) }) {
Surface(
@@ -95,10 +96,9 @@ internal fun UARTAddMacroDialog(onEvent: (UARTViewEvent) -> Unit) {
.background(background)
)
}
}
Spacer(modifier = Modifier.size(16.dp))
item(span = { GridItemSpan(GRID_SIZE) }) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
@@ -109,6 +109,12 @@ internal fun UARTAddMacroDialog(onEvent: (UARTViewEvent) -> Unit) {
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = { onEvent(OnDeleteMacro) }) {
Text(stringResource(id = R.string.uart_macro_dialog_delete))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isCommandValid(command.value)) {
onEvent(
@@ -130,6 +136,8 @@ internal fun UARTAddMacroDialog(onEvent: (UARTViewEvent) -> Unit) {
}
}
}
}
}
}
@Composable

View File

@@ -56,7 +56,8 @@ private fun createConfig(entries: List<String>, onResult: (StringListDialogResul
return StringListDialogConfig(
title = stringResource(id = R.string.uart_configuration_picker_dialog).toAnnotatedString(),
items = entries,
onResult = onResult
onResult = onResult,
leftIcon = R.drawable.ic_uart_settings
)
}

View File

@@ -1,37 +1,44 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTOutputRecord
import java.text.SimpleDateFormat
import java.util.*
@Composable
internal fun UARTContentView(state: UARTData, viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
internal fun UARTContentView(
state: UARTData,
viewState: UARTViewState,
onEvent: (UARTViewEvent) -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
OutputSection(state.text)
InputSection(viewState, onEvent)
Spacer(modifier = Modifier.height(16.dp))
InputSection(viewState, onEvent)
OutputSection(state.displayMessages, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -45,10 +52,15 @@ internal fun UARTContentView(state: UARTData, viewState: UARTViewState, onEvent:
@Composable
private fun InputSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showDialog = remember { mutableStateOf(false) }
val showAddDialog = rememberSaveable { mutableStateOf(false) }
val showDeleteDialog = rememberSaveable { mutableStateOf(false) }
if (showDialog.value) {
UARTAddConfigurationDialog(onEvent)
if (showAddDialog.value) {
UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false }
}
if (showDeleteDialog.value) {
DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false }
}
ScreenSection {
@@ -64,18 +76,33 @@ private fun InputSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> U
UARTConfigurationPicker(viewState, onEvent)
}
IconButton(onClick = { showDialog.value = true }) {
IconButton(onClick = { showAddDialog.value = true }) {
Icon(Icons.Default.Add, stringResource(id = R.string.uart_configuration_add))
}
viewState.selectedConfiguration?.let {
if (!viewState.isConfigurationEdited) {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(Icons.Default.Edit, stringResource(id = R.string.uart_configuration_edit))
Icon(
Icons.Default.Edit,
stringResource(id = R.string.uart_configuration_edit)
)
}
} else {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
painterResource(id = R.drawable.ic_pencil_off),
stringResource(id = R.string.uart_configuration_edit)
)
}
}
IconButton(onClick = { onEvent(OnDeleteConfiguration) }) {
Icon(Icons.Default.Delete, stringResource(id = R.string.uart_configuration_delete))
IconButton(onClick = { showDeleteDialog.value = true }) {
Icon(
Icons.Default.Delete,
stringResource(id = R.string.uart_configuration_delete)
)
}
}
}
@@ -90,16 +117,85 @@ private fun InputSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> U
}
@Composable
private fun OutputSection(text: String) {
private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.uart_delete_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(text = stringResource(id = R.string.uart_delete_dialog_info))
},
confirmButton = {
Button(onClick = {
onDismiss()
onEvent(OnDeleteConfiguration)
}) {
Text(text = stringResource(id = R.string.uart_delete_dialog_confirm))
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text(text = stringResource(id = R.string.uart_delete_dialog_cancel))
}
}
)
}
@Composable
private fun OutputSection(records: List<UARTOutputRecord>, onEvent: (UARTViewEvent) -> Unit) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(resId = R.drawable.ic_output, title = stringResource(R.string.uart_output))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SectionTitle(resId = R.drawable.ic_output, title = stringResource(R.string.uart_output), modifier = Modifier)
Icon(Icons.Default.Delete, contentDescription = "Clear items.", modifier = Modifier.clickable { onEvent(ClearOutputItems) })
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = text.ifBlank { stringResource(id = R.string.uart_output_placeholder) })
Column(modifier = Modifier.fillMaxWidth()) {
if (records.isEmpty()) {
Text(text = stringResource(id = R.string.uart_output_placeholder))
} else {
records.forEach {
MessageItem(record = it)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}
}
@Composable
private fun MessageItem(record: UARTOutputRecord) {
Column {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium
)
}
}
private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH)
private fun UARTOutputRecord.timeToString(): String {
return datFormatter.format(timestamp)
}

View File

@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -84,6 +85,7 @@ private fun MacroButton(
Image(
painter = painterResource(id = macro.icon.toResId()),
contentDescription = stringResource(id = R.string.uart_macro_icon),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary),
modifier = Modifier
.size(buttonSize)
.clip(RoundedCornerShape(10.dp))
@@ -104,7 +106,8 @@ private fun EmptyButton(
position: Int,
onEvent: (UARTViewEvent) -> Unit
) {
Box(modifier = Modifier
Box(
modifier = Modifier
.size(buttonSize)
.clip(RoundedCornerShape(10.dp))
.clickable {
@@ -112,7 +115,8 @@ private fun EmptyButton(
onEvent(OnEditMacro(position))
}
}
.background(getBackground(isEdited)))
.background(getBackground(isEdited))
)
}
@Composable
@@ -120,6 +124,6 @@ private fun getBackground(isEdited: Boolean): Color {
return if (!isEdited) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
MaterialTheme.colorScheme.tertiary
}
}

View File

@@ -26,7 +26,7 @@ fun UARTScreen() {
val state = viewModel.state.collectAsState().value
if (state.showEditDialog) {
UARTAddMacroDialog { viewModel.onEvent(it) }
UARTAddMacroDialog(state.selectedMacro) { viewModel.onEvent(it) }
}
Column {

View File

@@ -3,17 +3,24 @@ package no.nordicsemi.android.uart.view
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTMacro
internal data class UARTViewState(
val editedPosition: Int? = null,
val selectedConfigurationIndex: Int? = null,
val selectedConfigurationName: String? = null,
val isConfigurationEdited: Boolean = false,
val configurations: List<UARTConfiguration> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState
) {
val showEditDialog: Boolean = editedPosition != null
val selectedConfiguration: UARTConfiguration? = selectedConfigurationIndex?.let { configurations[it] }
val selectedConfiguration: UARTConfiguration? = configurations.find { selectedConfigurationName == it.name }
val selectedMacro: UARTMacro? = selectedConfiguration?.let { configuration ->
editedPosition?.let {
configuration.macros.getOrNull(it)
}
}
}
internal sealed class HTSManagerState

View File

@@ -7,7 +7,7 @@ internal sealed class UARTViewEvent
internal data class OnEditMacro(val position: Int) : UARTViewEvent()
internal data class OnCreateMacro(val macro: UARTMacro) : UARTViewEvent()
internal data class OnDeleteMacro(val macro: UARTMacro) : UARTViewEvent()
internal object OnDeleteMacro : UARTViewEvent()
internal object OnEditFinish : UARTViewEvent()
internal data class OnConfigurationSelected(val configuration: UARTConfiguration) : UARTViewEvent()
@@ -16,6 +16,7 @@ internal object OnEditConfiguration : UARTViewEvent()
internal object OnDeleteConfiguration : UARTViewEvent()
internal data class OnRunMacro(val macro: UARTMacro) : UARTViewEvent()
internal object ClearOutputItems : UARTViewEvent()
internal object DisconnectEvent : UARTViewEvent()
internal object NavigateUp : UARTViewEvent()

View File

@@ -1,66 +0,0 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTMacro
@Composable
internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) {
Card(backgroundColor = MaterialTheme.colorScheme.primaryContainer) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = stringResource(id = R.string.uart_run_macro_description),
modifier = Modifier
.size(70.dp)
.padding(8.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(OnRunMacro(macro)) }
)
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = Modifier.weight(1f).padding(vertical = 8.dp)) {
Text(
text = stringResource(id = R.string.uart_macro_dialog_selected_eol, macro.newLineChar.toDisplayString()),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
Text(
text = stringResource(id = R.string.uart_command_field, macro.command),
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.size(16.dp))
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(id = R.string.uart_delete_macro_description),
modifier = Modifier
.padding(8.dp)
.padding(end = 8.dp)
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(OnDeleteMacro(macro)) }
)
}
}
}

View File

@@ -64,7 +64,7 @@ internal class UARTViewModel @Inject constructor(
fun onEvent(event: UARTViewEvent) {
when (event) {
is OnCreateMacro -> addNewMacro(event.macro)
is OnDeleteMacro -> deleteMacro(event.macro)
OnDeleteMacro -> deleteMacro()
DisconnectEvent -> disconnect()
is OnRunMacro -> repository.runMacro(event.macro)
NavigateUp -> navigationManager.navigateUp()
@@ -74,16 +74,19 @@ internal class UARTViewModel @Inject constructor(
is OnAddConfiguration -> onAddConfiguration(event)
OnDeleteConfiguration -> deleteConfiguration()
OnEditConfiguration -> onEditConfiguration()
ClearOutputItems -> repository.clearItems()
}.exhaustive
}
private fun onEditConfiguration() {
_state.value = _state.value.copy(isConfigurationEdited = true)
val isEdited = _state.value.isConfigurationEdited
_state.value = _state.value.copy(isConfigurationEdited = !isEdited)
}
private fun onAddConfiguration(event: OnAddConfiguration) {
viewModelScope.launch(Dispatchers.IO) {
dataSource.saveConfiguration(UARTConfiguration(event.name))
dataSource.saveConfiguration(UARTConfiguration(null, event.name))
_state.value = _state.value.copy(selectedConfigurationName = event.name)
}
}
@@ -96,7 +99,7 @@ internal class UARTViewModel @Inject constructor(
}
private fun onConfigurationSelected(event: OnConfigurationSelected) {
_state.value = _state.value.copy(selectedConfigurationIndex = _state.value.configurations.indexOf(event.configuration))
_state.value = _state.value.copy(selectedConfigurationName = event.configuration.name)
}
private fun addNewMacro(macro: UARTMacro) {
@@ -106,13 +109,6 @@ internal class UARTViewModel @Inject constructor(
set(_state.value.editedPosition!!, macro)
}
val newConf = it.copy(macros = macros)
val newConfs = _state.value.configurations.map {
if (it.name == newConf.name) {
newConf
} else {
it
}
}
dataSource.saveConfiguration(newConf)
_state.value = _state.value.copy(editedPosition = null)
}
@@ -127,9 +123,16 @@ internal class UARTViewModel @Inject constructor(
}
}
private fun deleteMacro(macro: UARTMacro) {
private fun deleteMacro() {
viewModelScope.launch(Dispatchers.IO) {
// dataSource.deleteMacro(macro)
_state.value.selectedConfiguration?.let {
val macros = it.macros.toMutableList().apply {
set(_state.value.editedPosition!!, null)
}
val newConf = it.copy(macros = macros)
dataSource.saveConfiguration(newConf)
_state.value = _state.value.copy(editedPosition = null)
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18.66,2C18.4,2 18.16,2.09 17.97,2.28L16.13,4.13L19.88,7.88L21.72,6.03C22.11,5.64 22.11,5 21.72,4.63L19.38,2.28C19.18,2.09 18.91,2 18.66,2M3.28,4L2,5.28L8.5,11.75L4,16.25V20H7.75L12.25,15.5L18.72,22L20,20.72L13.5,14.25L9.75,10.5L3.28,4M15.06,5.19L11.03,9.22L14.78,12.97L18.81,8.94L15.06,5.19Z"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
android:pathData="M11,7A2,2 0,0 0,9 9V15A2,2 0,0 0,11 17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H11M11,13H13V15H11V13Z"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
android:pathData="M11,17L15,9V7H9V9H13L9,17"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
android:pathData="M11,13H13V15H11M11,9H13V11H11M11,17H13A2,2 0,0 0,15 15V13.5A1.5,1.5 0,0 0,13.5 12A1.5,1.5 0,0 0,15 10.5V9C15,7.89 14.1,7 13,7H11A2,2 0,0 0,9 9V10.5A1.5,1.5 0,0 0,10.5 12A1.5,1.5 0,0 0,9 13.5V15C9,16.11 9.9,17 11,17"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
android:pathData="M13,17A2,2 0,0 0,15 15V9A2,2 0,0 0,13 7H11A2,2 0,0 0,9 9V11A2,2 0,0 0,11 13H13V15H9V17H13M13,11H11V9H13V11Z"/>
</vector>

View File

@@ -22,11 +22,14 @@
<string name="uart_add_macro">Add macro</string>
<string name="uart_output_info">Here will be displayed read value from GATT characteristic.</string>
<string name="uart_configuration_dialog_title">Add configuration</string>
<string name="uart_configuration_hint">Configuration</string>
<string name="uart_macro_dialog_title">Add macro</string>
<string name="uart_macro_dialog_alias">Alias</string>
<string name="uart_macro_dialog_command">Command</string>
<string name="uart_macro_dialog_confirm">Confirm</string>
<string name="uart_macro_dialog_dismiss">Dismiss</string>
<string name="uart_macro_dialog_delete">Delete</string>
<string name="uart_macro_dialog_eol">EOL:</string>
<string name="uart_macro_dialog_selected_eol">EOL: %s</string>
@@ -41,4 +44,9 @@
<string name="uart_name_empty">Provided name cannot be empty.</string>
<string name="uart_macro_icon">Icon representing defined command.</string>
<string name="uart_delete_dialog_title">Delete configuration?</string>
<string name="uart_delete_dialog_info">Are you sure that you want to delete this configuration? Your data will be irretrievably lost.</string>
<string name="uart_delete_dialog_confirm">Confirm</string>
<string name="uart_delete_dialog_cancel">Cancel</string>
</resources>