diff --git a/TODO.md b/TODO.md index 26774324b..fbd2e329c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,8 @@ # High priority MVP features required for first public alpha -* start bt receive on boot +* test bt boot behavior +* fix BT device scanning - make a setup screen * when a text arrives, move that node info card to the bottom on the window - put the text to the left of the card. with a small arrow/distance/shortname * let the user type texts somewhere * include a background behind our cloud graphics, so redraws work properly @@ -13,7 +14,6 @@ MVP features required for first public alpha * make nodeinfo card not look like ass * at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later * connect to bluetooth device automatically using minimum power - start looking at phone boot -* fix BT device scanning * call crashlytics from exceptionReporter!!! currently not logging failures caught there * test with oldest compatible android in emulator (see below for testing with hardware) * make playstore entry, first public alpha @@ -94,3 +94,4 @@ Don't leave device discoverable. Don't let unpaired users do things with device * when notified phone should automatically download messages * use https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/#4 to show service state * all chat in the app defaults to group chat +* start bt receive on boot diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 4cca184d3..715194127 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -172,7 +172,7 @@ class MainActivity : AppCompatActivity(), Logging, /* Do this better FIXME */ val usetbeam = false val address = if (usetbeam) "B4:E6:2D:EA:32:B7" else "24:6F:28:96:C9:2A" - RadioInterfaceService.setBondedDeviceAddress(this, address) + RadioInterfaceService.setBondedDeviceAddress(this, null) requestPermission() } diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 718ac932e..5628b6b8e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -97,7 +97,9 @@ class RadioInterfaceService : Service(), Logging { */ const val RADIO_CONNECTED_ACTION = "$prefix.CONNECT_CHANGED" - private val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") + /// this service UUID is publically visible for scanning + val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") + private val BTM_FROMRADIO_CHARACTER = UUID.fromString("8ba2bcc2-ee02-4a55-a531-c525c5e454d5") private val BTM_TORADIO_CHARACTER = @@ -131,11 +133,19 @@ class RadioInterfaceService : Service(), Logging { private fun getPrefs(context: Context) = context.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE) - /// Return the device we are configured to use, or null for none - fun getBondedDeviceAddress(context: Context) = getPrefs(context).getString("devAddr", null) + private const val DEVADDR_KEY = "devAddr" - fun setBondedDeviceAddress(context: Context, addr: String) = - getPrefs(context).edit(commit = true) { putString("devAddr", addr) } + /// Return the device we are configured to use, or null for none + fun getBondedDeviceAddress(context: Context) = + getPrefs(context).getString(DEVADDR_KEY, null) + + fun setBondedDeviceAddress(context: Context, addr: String?) = + getPrefs(context).edit(commit = true) { + if (addr == null) + this.remove(DEVADDR_KEY) + else + putString(DEVADDR_KEY, addr) + } } private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanCard.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanCard.kt new file mode 100644 index 000000000..d2e719871 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanCard.kt @@ -0,0 +1,30 @@ +package com.geeksville.mesh.ui + +import androidx.compose.Composable +import androidx.compose.Model +import androidx.ui.core.Text +import androidx.ui.layout.Column +import androidx.ui.layout.Row +import androidx.ui.tooling.preview.Preview + +@Model +data class BTScanEntry(val name: String, val macAddress: String, var selected: Boolean) + +@Composable +fun BTScanCard(node: BTScanEntry) { + // Text("Node: ${it.user?.longName}") + Row { + Text(node.name) + + Text(node.selected.toString()) + } +} + +@Preview +@Composable +fun btScanPreview() { + Column { + BTScanCard(BTScanEntry("Meshtastic_ab12", "xx", true)) + BTScanCard(BTScanEntry("Meshtastic_32ac", "xx", false)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt new file mode 100644 index 000000000..4891f6870 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt @@ -0,0 +1,89 @@ +package com.geeksville.mesh.ui + +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.os.ParcelUuid +import androidx.compose.Composable +import androidx.compose.Context +import androidx.compose.ambient +import androidx.compose.onActive +import androidx.ui.core.ContextAmbient +import androidx.ui.core.Text +import androidx.ui.layout.Column +import androidx.ui.tooling.preview.Preview +import com.geeksville.android.Logging +import com.geeksville.mesh.service.RadioInterfaceService + +object BTLog : Logging + +@Composable +fun BTScanScreen() { + val context = ambient(ContextAmbient) + + /// Note: may be null on platforms without a bluetooth driver (ie. the emulator) + val bluetoothAdapter = + (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter + + onActive { + + if (bluetoothAdapter == null) + BTLog.warn("No bluetooth adapter. Running under emulation?") + else { + val scanner = bluetoothAdapter.bluetoothLeScanner + + val scanCallback = object : ScanCallback() { + override fun onScanFailed(errorCode: Int) { + TODO() // FIXME, update gui with message about this + } + + // For each device that appears in our scan, ask for its GATT, when the gatt arrives, + // check if it is an eligable device and store it in our list of candidates + // if that device later disconnects remove it as a candidate + override fun onScanResult(callbackType: Int, result: ScanResult) { + + BTLog.info("onScanResult ${result.device.address}") + + // We don't need any more results now + // scanner.stopScan(this) + } + } + + BTLog.debug("starting scan") + + // filter and only accept devices that have a sw update service + val filter = + ScanFilter.Builder() + .setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID)) + .build() + + /* ScanSettings.CALLBACK_TYPE_FIRST_MATCH seems to trigger a bug returning an error of + SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES (error #5) + */ + val settings = + ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY). + // setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT). + // setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH). + build() + scanner.startScan(listOf(filter), settings, scanCallback) + + onDispose { + BTLog.debug("stopping scan") + scanner.stopScan(scanCallback) + } + } + } + + Column { + Text("FIXME") + } +} + + +@Preview +@Composable +fun btScanScreenPreview() { + BTScanScreen() +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index 2a2c2fd1a..ebf0e2e3e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -20,9 +20,6 @@ import com.geeksville.mesh.R @Composable fun HomeContent() { Column { - Text(text = "Meshtastic") - - Row { Container(LayoutSize(40.dp, 40.dp)) { VectorImage(id = if (UIState.isConnected.value) R.drawable.cloud_on else R.drawable.cloud_off) @@ -57,23 +54,6 @@ fun HomeContent() { } } -@Composable -fun HomeScreen(openDrawer: () -> Unit) { - Column { - TopAppBar( - title = { Text(text = "Meshtastic") }, - navigationIcon = { - VectorImageButton(R.drawable.ic_launcher_new_foreground) { - openDrawer() - } - } - ) - VerticalScroller(modifier = LayoutFlexible(1f)) { - HomeContent() - } - } -} - @Composable fun MeshApp() { @@ -108,10 +88,26 @@ fun previewView() { private fun AppContent(openDrawer: () -> Unit) { Crossfade(AppStatus.currentScreen) { screen -> Surface(color = (MaterialTheme.colors()).background) { - when (screen) { - is Screen.Home -> HomeScreen { openDrawer() } - /* is Screen.Interests -> InterestsScreen { openDrawer() } - is Screen.Article -> ArticleScreen(postId = screen.postId) */ + + Column { + TopAppBar( + title = { Text(text = "Meshtastic") }, + navigationIcon = { + VectorImageButton(R.drawable.ic_launcher_new_foreground) { + openDrawer() + } + } + ) + + VerticalScroller(modifier = LayoutFlexible(1f)) { + when (screen) { + is Screen.Home -> HomeContent() + is Screen.SelectRadio -> BTScanScreen() + // Question: how to get hooks invoked when this screen gets shown/removed? + // i.e. I need to start/stop a bluetooth scan operation. depending on the + // appearance/disappearance of this screen. + } + } } } } @@ -133,25 +129,21 @@ private fun AppDrawer( VectorImage(id = R.drawable.ic_launcher_new_foreground) } Divider(color = Color(0x14333333)) - DrawerButton( - icon = R.drawable.ic_launcher_new_foreground, - label = "Home", - isSelected = currentScreen == Screen.Home - ) { - navigateTo(Screen.Home) - closeDrawer() + + @Composable + fun ScreenButton(icon: Int, label: String, screen: Screen) { + DrawerButton( + icon = icon, + label = label, + isSelected = currentScreen == screen + ) { + navigateTo(screen) + closeDrawer() + } } - /* - DrawerButton( - icon = R.drawable.ic_interests, - label = "Interests", - isSelected = currentScreen == Screen.Interests - ) { - navigateTo(Screen.Interests) - closeDrawer() - } - */ + ScreenButton(R.drawable.ic_launcher_new_foreground, "Home", Screen.Home) + ScreenButton(R.drawable.ic_launcher_new_foreground, "Setup", Screen.SelectRadio) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Status.kt b/app/src/main/java/com/geeksville/mesh/ui/Status.kt index fe20b93f0..097b34257 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Status.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Status.kt @@ -10,7 +10,7 @@ import java.util.* // defines the screens we have in the app sealed class Screen { object Home : Screen() - // object Settings : Screen() + object SelectRadio : Screen() } @Model @@ -22,7 +22,7 @@ data class TextMessage(val date: Date, val from: String, val text: String) /// FIXME - figure out how to merge this staate with the AppStatus Model object UIState { - + private val testPositions = arrayOf( Position(32.776665, -96.796989, 35), // dallas Position(32.960758, -96.733521, 35), // richardson diff --git a/app/src/main/proto/mesh.options b/app/src/main/proto/mesh.options index 62503d11c..1350a970d 100644 --- a/app/src/main/proto/mesh.options +++ b/app/src/main/proto/mesh.options @@ -12,7 +12,7 @@ *DeviceState.receive_queue max_count:32 # FIXME, max out based on total SubPacket size And do fragmentation and reassembly (for larger payloads) at the Android layer, not the esp32 layer. -*Data.payload max_size:200 +*Data.payload max_size:251 # 128 bit psk key (we don't use 256 bit yet because we want to keep our QR code small) *ChannelSettings.psk max_size:16 fixed_length:true diff --git a/app/src/main/proto/mesh.proto b/app/src/main/proto/mesh.proto index 52f6f61fe..6aba9ffeb 100644 --- a/app/src/main/proto/mesh.proto +++ b/app/src/main/proto/mesh.proto @@ -301,7 +301,11 @@ message DeviceState { Current = 11; }; + /// A version integer used to invalidate old save files when we make incompatible changes Version version = 6; + + /// We keep the last received text message (only) stored in the device flash, so we can show it on the screen. Might be null + MeshPacket rx_text_message = 7; } // packets from the radio to the phone will appear on the fromRadio characteristic. It will support