Add connect, pause, sync

This commit is contained in:
Patrick Moessler 2019-10-25 02:08:13 +02:00
parent 173e6690c2
commit eaa6d72a3e
8 changed files with 339 additions and 79 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="JDK" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -0,0 +1,89 @@
package de.asaril.bletail
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.Intent
import android.util.Log
import no.nordicsemi.android.ble.callback.SuccessCallback
class BleConnectionHandler(main: MainActivity) {
private var ble: UartManager? = null
private val activity: MainActivity = main
var connected = false
private set
private fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
fun connect(callback: ((Boolean) -> Unit)?) {
ble = UartManager(activity)
ble!!.setGattCallbacks(DefaultUartManagerCallbacks())
val bluetoothManager: BluetoothManager =
activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
if (!bluetoothAdapter.isEnabled) {
val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
val requestEnableBt = 1
activity.startActivityForResult(enableIntent, requestEnableBt)
}
bluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
Log.i("BLEtail", "Remote device name: " + result.device.name)
if (result.device.name == "Bluefruit52") {
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
ble!!.connect(result.device)
.retry(3, 100)
.useAutoConnect(true)
.done {
connected = true
if (callback != null) {
callback(connected)
}
}
.fail { _: BluetoothDevice, _: Int ->
connected = false
if (callback != null) {
callback(connected)
}
}
.enqueue()
}
}
})
}
fun disconnect(callback: ((Boolean) -> Unit)?) {
ble!!.disconnect()
.done {
connected = false
if (callback != null) {
callback(connected)
}
}
.enqueue()
}
fun sendToBleUart(msgRaw: ByteArray) {
if (connected) {
val arr: ByteArray = CobsUtils.encode(msgRaw)
Log.i("BLEtail", "tx:" + arr.toHexString())
ble?.send(arr)
} else {
Log.w("BLEtail", "tx failed (not connected):" + msgRaw.toHexString())
}
}
}

View file

@ -1,18 +1,9 @@
package de.asaril.bletail package de.asaril.bletail
import CobsUtils
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.SeekBar import android.widget.SeekBar
@ -23,56 +14,46 @@ import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import android.view.MenuItem
import android.view.View
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val LED_COUNT: Int = 40 private val LED_COUNT: Int = 40
private var ble: UartManager? = null private var ble: BleConnectionHandler? = null
private var wasConnected = false
private var segments: MutableList<SegmentCardView>? = null private var segments: MutableList<SegmentCardView>? = null
private var paused = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
ble = BleConnectionHandler(this)
segments = MutableList<SegmentCardView>(5, init = { segments = MutableList<SegmentCardView>(5, init = {
SegmentCardView(this, it, 0, LED_COUNT - 1, Fx.Mode.THEATER_CHASE_RAINBOW, Color(), Color(), Color(), 0x0800, false) SegmentCardView(this, it, 0, LED_COUNT - 1, Fx.Mode.THEATER_CHASE_RAINBOW, Color(), Color(), Color(), 0x0800, false)
}) })
for (s in segments!!) { for (s in segments!!) {
s.sendCallback = this::sendToBleUart s.sendCallback = { bytes: ByteArray -> ble!!.sendToBleUart(bytes) }
} }
ble!!.connect { connected: Boolean ->
ble = UartManager(this) if (connected) {
ble!!.setGattCallbacks(DefaultUartManagerCallbacks()) val storedState = File(getExternalFilesDir(null)!!, "_stored.json")
val loaded = storedState.exists() and loadFile(storedState)
val bluetoothManager: BluetoothManager = if (!loaded) {
getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val defaultFile = File(getExternalFilesDir(null)!!, "default.json")
val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter if (defaultFile.exists()) {
loadFile(defaultFile)
if (!bluetoothAdapter.isEnabled) { }
val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
val requestEnableBt = 1
startActivityForResult(enableIntent, requestEnableBt)
}
bluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
Log.i("BLEtail", "Remote device name: " + result.device.name)
if (result.device.name == "Bluefruit52") {
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
ble!!.connect(result.device)
.retry(3, 100)
.useAutoConnect(true)
.enqueue()
} }
} }
}) updateConnection(connected)
}
brightness!!.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { brightness!!.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(var1: SeekBar, var2: Int, var3: Boolean) { override fun onProgressChanged(var1: SeekBar, var2: Int, var3: Boolean) {
@ -90,15 +71,49 @@ class MainActivity : AppCompatActivity() {
} }
override fun onResume() {
if (wasConnected) {
ble?.connect { c -> updateConnection(c) }
}
super.onResume()
}
override fun onPause() {
saveFile("_stored")
super.onPause()
}
override fun onStop() {
wasConnected = (ble?.connected) ?: false
ble?.disconnect { c -> updateConnection(c) }
super.onStop()
}
override fun onDestroy() { override fun onDestroy() {
segments = null segments = null
ble!!.disconnect() ble!!.disconnect { c -> updateConnection(c) }
val storedState = File(getExternalFilesDir(null)!!, "_stored.json")
if (storedState.exists()) {
storedState.deleteOnExit()
}
super.onDestroy() super.onDestroy()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present. // Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main_menu, menu) menuInflater.inflate(R.menu.main_menu, menu)
val connected = (ble != null) and (ble!!.connected)
menu.findItem(R.id.action_connection).apply {
if (connected) {
icon = getDrawable(R.drawable.ic_bluetooth_connected_black_24dp)
title = getString(R.string.disconnect)
} else {
icon = getDrawable(R.drawable.ic_bluetooth_disabled_black_24dp)
title = getString(R.string.connect)
}
}
return true return true
} }
@ -107,28 +122,30 @@ class MainActivity : AppCompatActivity() {
val m = Fx.setNumSegments_msg.newBuilder() val m = Fx.setNumSegments_msg.newBuilder()
m.numSegments = count m.numSegments = count
r.setNumSegments = m.build() r.setNumSegments = m.build()
sendToBleUart(r.build().toByteArray()) if (ble != null) {
ble!!.sendToBleUart(r.build().toByteArray())
val segLength: Int = LED_COUNT / count val segLength: Int = LED_COUNT / count
val segmentListLayout = findViewById<LinearLayout>(R.id.segmentList) val segmentListLayout = findViewById<LinearLayout>(R.id.segmentList)
for (i in 0 until count) { for (i in 0 until count) {
segments!![i].start = segLength * i segments!![i].start = segLength * i
segments!![i].end = if (i == count - 1) { segments!![i].end = if (i == count - 1) {
LED_COUNT - 1 LED_COUNT - 1
} else { } else {
(segLength * (i + 1) - 1) (segLength * (i + 1) - 1)
}
segments!![i].sendSetSegment()
} }
segments!![i].sendSetSegment()
}
while (segmentListLayout.childCount > count) { while (segmentListLayout.childCount > count) {
val ind = segmentListLayout.childCount - 1 val ind = segmentListLayout.childCount - 1
segmentListLayout.removeViewAt(ind) segmentListLayout.removeViewAt(ind)
} }
while (segmentListLayout.childCount < count) { while (segmentListLayout.childCount < count) {
val ind = segmentListLayout.childCount val ind = segmentListLayout.childCount
segmentListLayout.addView(segments!![ind], ind) segmentListLayout.addView(segments!![ind], ind)
}
} }
} }
@ -141,8 +158,9 @@ class MainActivity : AppCompatActivity() {
val r: Fx.Root.Builder = Fx.Root.newBuilder() val r: Fx.Root.Builder = Fx.Root.newBuilder()
r.halt = Fx.halt_msg.newBuilder().build() r.halt = Fx.halt_msg.newBuilder().build()
val msgRaw = r.build().toByteArray() val msgRaw = r.build().toByteArray()
sendToBleUart(msgRaw) ble?.sendToBleUart(msgRaw)
ble!!.disconnect() ble?.disconnect(null)
finish()
} }
} }
builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
@ -154,7 +172,7 @@ class MainActivity : AppCompatActivity() {
val m = Fx.setBrightness_msg.newBuilder() val m = Fx.setBrightness_msg.newBuilder()
m.brightness = value m.brightness = value
r.setBrightness = m.build() r.setBrightness = m.build()
sendToBleUart(r.build().toByteArray()) ble?.sendToBleUart(r.build().toByteArray())
} }
fun segCountClick(item: MenuItem) { fun segCountClick(item: MenuItem) {
@ -169,13 +187,6 @@ class MainActivity : AppCompatActivity() {
) )
} }
fun sendToBleUart(msgRaw: ByteArray) {
val arr: ByteArray = CobsUtils.encode(msgRaw)
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
Log.i("BLEtail", "tx:" + arr.toHexString())
ble?.send(arr)
}
fun serialize(): JSONObject { fun serialize(): JSONObject {
val j = JSONObject() val j = JSONObject()
@ -189,21 +200,32 @@ class MainActivity : AppCompatActivity() {
return j return j
} }
fun deserialize(j: JSONObject) { fun deserialize(j: JSONObject): Boolean {
brightness.progress = j.getInt("brightness") brightness.progress = j.getInt("brightness")
val jsegs = j.getJSONArray("segments") val jsegs = j.getJSONArray("segments")
updateSegmentCards(jsegs.length()) val segNum = jsegs.length()
for (i in 0 until jsegs.length()) { if (segNum < 1) {
return false
}
updateSegmentCards(segNum)
for (i in 0 until segNum) {
val jseg = jsegs[i] as JSONObject val jseg = jsegs[i] as JSONObject
val idx = jseg.getInt("index") val idx = jseg.getInt("index")
assert(idx < segments!!.size) assert(idx < segments!!.size)
segments!![idx].deserialize(jseg) segments!![idx].deserialize(jseg)
} }
return true
} }
private fun loadFile(file: File) { private fun loadFile(file: File): Boolean {
val j = JSONObject(file.readText()) val j = JSONObject(file.readText())
deserialize(j) val loaded = deserialize(j)
if ((!loaded) and (file.name == "_stored.json")) {
// remove broken stored state
file.delete()
return false
}
return loaded
} }
private fun saveFile(filename: String) { private fun saveFile(filename: String) {
@ -240,5 +262,99 @@ class MainActivity : AppCompatActivity() {
builder.show() builder.show()
} }
fun pauseClick(item: MenuItem) {
if ((ble != null) && (ble!!.connected)) {
this.paused = !this.paused
sendPause(item)
}
}
private fun sendPause(item: MenuItem) {
val r = Fx.Root.newBuilder()
if (paused) {
val m = Fx.pause_msg.newBuilder()
r.pause = m.build()
item.icon = getDrawable(android.R.drawable.ic_media_play)
item.title = getString(R.string.resume)
} else {
val m = Fx.resume_msg.newBuilder()
r.resume = m.build()
item.icon = getDrawable(android.R.drawable.ic_media_pause)
item.title = getString(R.string.pause)
}
ble?.sendToBleUart(r.build().toByteArray())
}
fun updateConnection(connected: Boolean) {
(findViewById<View?>(R.id.action_connection) as MenuItem?)?.apply {
if (connected) {
icon = getDrawable(R.drawable.ic_bluetooth_connected_black_24dp)
title = getString(R.string.disconnect)
} else {
icon = getDrawable(R.drawable.ic_bluetooth_disabled_black_24dp)
title = getString(R.string.connect)
}
}
}
fun connectionClick(item: MenuItem) {
val callback = { connected: Boolean ->
item.apply {
if (connected) {
icon = getDrawable(R.drawable.ic_bluetooth_connected_black_24dp)
title = getString(R.string.disconnect)
} else {
icon = getDrawable(R.drawable.ic_bluetooth_disabled_black_24dp)
title = getString(R.string.connect)
}
}
Unit
}
if (ble != null) {
if (ble!!.connected) {
ble!!.disconnect(callback)
} else {
ble!!.connect(callback)
}
}
}
fun stopClick(item: MenuItem) {
if ((ble != null) && (ble!!.connected)) {
paused = true
sendPause(item)
val r = Fx.Root.newBuilder()
val m = Fx.strip_off_msg.newBuilder()
r.stripOff = m.build()
ble?.sendToBleUart(r.build().toByteArray())
}
val storedState = File(getExternalFilesDir(null)!!, "_stored.json")
if (storedState.exists()) {
storedState.deleteOnExit()
}
}
fun syncClick(item: MenuItem) {
if ((ble != null) && (ble!!.connected) && (segments != null)) {
var r = Fx.Root.newBuilder()
val m = Fx.stop_msg.newBuilder()
r.stop = m.build()
ble?.sendToBleUart(r.build().toByteArray())
for (i in 0 until segments!!.size) {
segments!![i].speed = segments!![0].speed
segments!![i].updateView()
}
r = Fx.Root.newBuilder()
val m2 = Fx.start_msg.newBuilder()
r.start = m2.build()
ble?.sendToBleUart(r.build().toByteArray())
}
}
} }

View file

@ -266,17 +266,22 @@ class SegmentCardView : CardView {
assert(this.index == j.getInt("index")) assert(this.index == j.getInt("index"))
this.start = j.getInt("start") this.start = j.getInt("start")
this.end = j.getInt("end") this.end = j.getInt("end")
this.mode = Fx.Mode.forNumber(j.getInt("mode"))
val jcolors = j.getJSONArray("colors") val jcolors = j.getJSONArray("colors")
this.colors[0] = Color.valueOf(jcolors[0] as Int) this.colors[0] = Color.valueOf(jcolors[0] as Int)
this.colors[1] = Color.valueOf(jcolors[1] as Int) this.colors[1] = Color.valueOf(jcolors[1] as Int)
this.colors[2] = Color.valueOf(jcolors[2] as Int) this.colors[2] = Color.valueOf(jcolors[2] as Int)
this.speed = j.getInt("speed") this.speed = j.getInt("speed")
this.reverse = j.getBoolean("reverse") this.reverse = j.getBoolean("reverse")
// set mode last as this will trigger setSegment
this.mode = Fx.Mode.forNumber(j.getInt("mode"))
updateView() updateView()
sendSetSegment()
} }
private fun updateView() { fun updateView() {
viewCol0!!.setColorFilter(colors[0].toArgb()) viewCol0!!.setColorFilter(colors[0].toArgb())
viewCol1!!.setColorFilter(colors[1].toArgb()) viewCol1!!.setColorFilter(colors[1].toArgb())
viewCol2!!.setColorFilter(colors[2].toArgb()) viewCol2!!.setColorFilter(colors[2].toArgb())

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.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,12l-2,-2 -2,2 2,2 2,-2zM17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88zM19,10l-2,2 2,2 2,-2 -2,-2z"/>
</vector>

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.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M13,5.83l1.88,1.88 -1.6,1.6 1.41,1.41 3.02,-3.02L12,2h-1v5.03l2,2v-3.2zM5.41,4L4,5.41 10.59,12 5,17.59 6.41,19 11,14.41V22h1l4.29,-4.29 2.3,2.29L20,18.59 5.41,4zM13,18.17v-3.76l1.88,1.88L13,18.17z"/>
</vector>

View file

@ -9,14 +9,28 @@
android:iconTint="@android:color/secondary_text_dark" android:iconTint="@android:color/secondary_text_dark"
android:title="@string/load" android:title="@string/load"
android:onClick="loadBtnClick" android:onClick="loadBtnClick"
app:showAsAction="ifRoom" /> app:showAsAction="always" />
<item <item
android:id="@+id/saveBtn" android:id="@+id/saveBtn"
android:title="@string/save" android:title="@string/save"
android:icon="@android:drawable/ic_menu_save" android:icon="@android:drawable/ic_menu_save"
android:iconTint="@android:color/primary_text_dark" android:iconTint="@android:color/primary_text_dark"
android:onClick="saveBtnClick" android:onClick="saveBtnClick"
app:showAsAction="ifRoom"/> app:showAsAction="always"/>
<item
android:id="@+id/action_pause"
android:icon="@android:drawable/ic_media_pause"
android:onClick="pauseClick"
android:title="@string/pause"
app:showAsAction="always" />
<item
android:id="@+id/action_connection"
android:icon="@drawable/ic_bluetooth_disabled_black_24dp"
android:onClick="connectionClick"
android:title="@string/connect"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/segCountMenu" android:id="@+id/segCountMenu"
@ -48,8 +62,20 @@
</menu> </menu>
</item> </item>
<item <item
android:id="@+id/action_halt" android:id="@+id/action_sync"
android:icon="@android:drawable/ic_popup_sync"
android:onClick="syncClick"
android:title="@string/action_sync"
app:showAsAction="collapseActionView" />
<item
android:id="@+id/action_stop"
android:icon="@android:drawable/ic_lock_power_off" android:icon="@android:drawable/ic_lock_power_off"
android:onClick="stopClick"
android:title="@string/action_stop"
app:showAsAction="collapseActionView" />
<item
android:id="@+id/action_halt"
android:icon="@android:drawable/ic_delete"
android:onClick="powerOffClick" android:onClick="powerOffClick"
android:title="@string/action_halt" android:title="@string/action_halt"
app:showAsAction="collapseActionView" /> app:showAsAction="collapseActionView" />

View file

@ -1,6 +1,6 @@
<resources> <resources>
<string name="app_name">BLEtail</string> <string name="app_name">BLEtail</string>
<string name="action_halt">Halt</string> <string name="action_halt">Shutdown</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="seg3">3</string> <string name="seg3">3</string>
<string name="seg5">5</string> <string name="seg5">5</string>
@ -15,4 +15,10 @@
<string name="brightness">Brightness</string> <string name="brightness">Brightness</string>
<string name="load">Load</string> <string name="load">Load</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="connect">Connect</string>
<string name="disconnect">Disconnect</string>
<string name="pause">Pause</string>
<string name="resume">Resume</string>
<string name="action_stop">Stop (Off)</string>
<string name="action_sync">Sync</string>
</resources> </resources>