diff --git a/.idea/misc.xml b/.idea/misc.xml index dfd2c79..b6ea2b1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/java/de/asaril/bletail/BleConnectionHandler.kt b/app/src/main/java/de/asaril/bletail/BleConnectionHandler.kt new file mode 100644 index 0000000..a413f58 --- /dev/null +++ b/app/src/main/java/de/asaril/bletail/BleConnectionHandler.kt @@ -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()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/asaril/bletail/MainActivity.kt b/app/src/main/java/de/asaril/bletail/MainActivity.kt index 584ca74..2ac7afc 100644 --- a/app/src/main/java/de/asaril/bletail/MainActivity.kt +++ b/app/src/main/java/de/asaril/bletail/MainActivity.kt @@ -1,18 +1,9 @@ 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.os.Bundle import android.text.InputType -import android.util.Log import android.view.Menu -import android.view.MenuItem import android.widget.EditText import android.widget.LinearLayout import android.widget.SeekBar @@ -23,56 +14,46 @@ import kotlinx.android.synthetic.main.activity_main.* import org.json.JSONArray import org.json.JSONObject import java.io.File - +import android.view.MenuItem +import android.view.View class MainActivity : AppCompatActivity() { private val LED_COUNT: Int = 40 - private var ble: UartManager? = null + private var ble: BleConnectionHandler? = null + private var wasConnected = false private var segments: MutableList? = null + private var paused = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) + ble = BleConnectionHandler(this) + segments = MutableList(5, init = { SegmentCardView(this, it, 0, LED_COUNT - 1, Fx.Mode.THEATER_CHASE_RAINBOW, Color(), Color(), Color(), 0x0800, false) }) for (s in segments!!) { - s.sendCallback = this::sendToBleUart + s.sendCallback = { bytes: ByteArray -> ble!!.sendToBleUart(bytes) } } - - ble = UartManager(this) - ble!!.setGattCallbacks(DefaultUartManagerCallbacks()) - - val bluetoothManager: BluetoothManager = - getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter - - 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() + ble!!.connect { connected: Boolean -> + if (connected) { + val storedState = File(getExternalFilesDir(null)!!, "_stored.json") + val loaded = storedState.exists() and loadFile(storedState) + if (!loaded) { + val defaultFile = File(getExternalFilesDir(null)!!, "default.json") + if (defaultFile.exists()) { + loadFile(defaultFile) + } } } - }) + updateConnection(connected) + } brightness!!.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 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() { segments = null - ble!!.disconnect() + ble!!.disconnect { c -> updateConnection(c) } + val storedState = File(getExternalFilesDir(null)!!, "_stored.json") + if (storedState.exists()) { + storedState.deleteOnExit() + } super.onDestroy() } override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. 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 } @@ -107,28 +122,30 @@ class MainActivity : AppCompatActivity() { val m = Fx.setNumSegments_msg.newBuilder() m.numSegments = count r.setNumSegments = m.build() - sendToBleUart(r.build().toByteArray()) + if (ble != null) { + ble!!.sendToBleUart(r.build().toByteArray()) - val segLength: Int = LED_COUNT / count - val segmentListLayout = findViewById(R.id.segmentList) + val segLength: Int = LED_COUNT / count + val segmentListLayout = findViewById(R.id.segmentList) - for (i in 0 until count) { - segments!![i].start = segLength * i - segments!![i].end = if (i == count - 1) { - LED_COUNT - 1 - } else { - (segLength * (i + 1) - 1) + for (i in 0 until count) { + segments!![i].start = segLength * i + segments!![i].end = if (i == count - 1) { + LED_COUNT - 1 + } else { + (segLength * (i + 1) - 1) + } + segments!![i].sendSetSegment() } - segments!![i].sendSetSegment() - } - while (segmentListLayout.childCount > count) { - val ind = segmentListLayout.childCount - 1 - segmentListLayout.removeViewAt(ind) - } - while (segmentListLayout.childCount < count) { - val ind = segmentListLayout.childCount - segmentListLayout.addView(segments!![ind], ind) + while (segmentListLayout.childCount > count) { + val ind = segmentListLayout.childCount - 1 + segmentListLayout.removeViewAt(ind) + } + while (segmentListLayout.childCount < count) { + val ind = segmentListLayout.childCount + segmentListLayout.addView(segments!![ind], ind) + } } } @@ -141,8 +158,9 @@ class MainActivity : AppCompatActivity() { val r: Fx.Root.Builder = Fx.Root.newBuilder() r.halt = Fx.halt_msg.newBuilder().build() val msgRaw = r.build().toByteArray() - sendToBleUart(msgRaw) - ble!!.disconnect() + ble?.sendToBleUart(msgRaw) + ble?.disconnect(null) + finish() } } builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } @@ -154,7 +172,7 @@ class MainActivity : AppCompatActivity() { val m = Fx.setBrightness_msg.newBuilder() m.brightness = value r.setBrightness = m.build() - sendToBleUart(r.build().toByteArray()) + ble?.sendToBleUart(r.build().toByteArray()) } 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 { val j = JSONObject() @@ -189,21 +200,32 @@ class MainActivity : AppCompatActivity() { return j } - fun deserialize(j: JSONObject) { + fun deserialize(j: JSONObject): Boolean { brightness.progress = j.getInt("brightness") val jsegs = j.getJSONArray("segments") - updateSegmentCards(jsegs.length()) - for (i in 0 until jsegs.length()) { + val segNum = jsegs.length() + if (segNum < 1) { + return false + } + updateSegmentCards(segNum) + for (i in 0 until segNum) { val jseg = jsegs[i] as JSONObject val idx = jseg.getInt("index") assert(idx < segments!!.size) segments!![idx].deserialize(jseg) } + return true } - private fun loadFile(file: File) { + private fun loadFile(file: File): Boolean { 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) { @@ -240,5 +262,99 @@ class MainActivity : AppCompatActivity() { 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(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()) + } + } } diff --git a/app/src/main/java/de/asaril/bletail/SegmentCardView.kt b/app/src/main/java/de/asaril/bletail/SegmentCardView.kt index 059d620..309b707 100644 --- a/app/src/main/java/de/asaril/bletail/SegmentCardView.kt +++ b/app/src/main/java/de/asaril/bletail/SegmentCardView.kt @@ -266,17 +266,22 @@ class SegmentCardView : CardView { assert(this.index == j.getInt("index")) this.start = j.getInt("start") this.end = j.getInt("end") - this.mode = Fx.Mode.forNumber(j.getInt("mode")) + val jcolors = j.getJSONArray("colors") this.colors[0] = Color.valueOf(jcolors[0] as Int) this.colors[1] = Color.valueOf(jcolors[1] as Int) this.colors[2] = Color.valueOf(jcolors[2] as Int) this.speed = j.getInt("speed") + this.reverse = j.getBoolean("reverse") + + // set mode last as this will trigger setSegment + this.mode = Fx.Mode.forNumber(j.getInt("mode")) updateView() + sendSetSegment() } - private fun updateView() { + fun updateView() { viewCol0!!.setColorFilter(colors[0].toArgb()) viewCol1!!.setColorFilter(colors[1].toArgb()) viewCol2!!.setColorFilter(colors[2].toArgb()) diff --git a/app/src/main/res/drawable/ic_bluetooth_connected_black_24dp.xml b/app/src/main/res/drawable/ic_bluetooth_connected_black_24dp.xml new file mode 100644 index 0000000..c81c3a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_bluetooth_connected_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bluetooth_disabled_black_24dp.xml b/app/src/main/res/drawable/ic_bluetooth_disabled_black_24dp.xml new file mode 100644 index 0000000..1f753a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bluetooth_disabled_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 29eacc4..d31c881 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -9,14 +9,28 @@ android:iconTint="@android:color/secondary_text_dark" android:title="@string/load" android:onClick="loadBtnClick" - app:showAsAction="ifRoom" /> + app:showAsAction="always" /> + app:showAsAction="always"/> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index caef947..dfd58c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ BLEtail - Halt + Shutdown Settings 3 5 @@ -15,4 +15,10 @@ Brightness Load Save + Connect + Disconnect + Pause + Resume + Stop (Off) + Sync