From 559795b79671883e460aa04f132dd46c4ba27529 Mon Sep 17 00:00:00 2001 From: geeksville Date: Fri, 24 Jan 2020 17:05:55 -0800 Subject: [PATCH] add beginnings of radio interface service --- README.md | 2 +- .../mesh/ExampleInstrumentedTest.kt | 2 +- app/src/main/AndroidManifest.xml | 18 ++-- .../com/geeksville/mesh/IMeshService.aidl | 4 +- .../java/com/geeksville/mesh/MeshService.kt | 55 ++++++++--- .../geeksville/mesh/MeshUtilApplication.kt | 5 +- .../geeksville/mesh/RadioInterfaceService.kt | 93 +++++++++++++++++++ .../geeksville/mesh/SoftwareUpdateService.kt | 23 +---- app/src/main/proto/mesh.proto | 91 +++++++++--------- 9 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt diff --git a/README.md b/README.md index 1fbdc5287..c5e52b229 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Questions? kevinh@geeksville.com Once this project is public, I'll happily let collaborators have access to the crash logs/analytics. * analytics is currently on, before beta is over I'll make it optional -* on dev devices "adb shell setprop debug.firebase.analytics.app com.geeeksville.mesh" +* on dev devices "adb shell setprop debug.firebase.analytics.app com.geeksville.mesh" * To see analytics: https://console.firebase.google.com/u/0/project/meshutil/analytics/app/android:com.geeksville.mesh/overview * To see crash logs: https://console.firebase.google.com/u/0/project/meshutil/crashlytics/app/android:com.geeksville.mesh/issues?state=open&time=last-seven-days&type=crash diff --git a/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt index 64799310d..2ae926b86 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/ExampleInstrumentedTest.kt @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.geeksville.com.geeeksville.mesh", appContext.packageName) + assertEquals("com.geeksville.com.geeksville.mesh", appContext.packageName) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9643c683a..782380cc9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,15 +16,13 @@ This permission is required to allow the application to send events and properties to Mixpanel. --> - + - + - @@ -49,11 +48,18 @@ android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE" /> + + + + - + diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index b7025bd72..68687fb3d 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -1,4 +1,4 @@ -// com.geeksville.com.geeeksville.mesh.IMeshService.aidl +// com.geeksville.mesh.IMeshService.aidl package com.geeksville.mesh; // Declare any non-default types here with import statements @@ -24,7 +24,7 @@ interface IMeshService { */ boolean isConnected(); - // see com.geeksville.com.geeeksville.mesh broadcast intents + // see com.geeksville.com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes // NODE_CHANGE for new IDs appearing or disappearing // CONNECTION_CHANGED for losing/gaining connection to the packet radio diff --git a/app/src/main/java/com/geeksville/mesh/MeshService.kt b/app/src/main/java/com/geeksville/mesh/MeshService.kt index 42419a2d0..9f540f9c2 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshService.kt @@ -1,42 +1,45 @@ package com.geeksville.mesh import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.IBinder import com.geeksville.android.Logging + /** + * Handles all the communication with android apps. Also keeps an internal model + * of the network state. + * * Note: this service will go away once all clients are unbound from it. */ class MeshService : Service(), Logging { - companion object { - const val prefix = "com.geeksville.mesh" - } - /* see com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes // NODE_CHANGE for new IDs appearing or disappearing // CONNECTION_CHANGED for losing/gaining connection to the packet radio */ + + /** + * The RECEIVED_OPAQUE: + * Payload will be the raw bytes which were contained within a MeshPacket.Opaque field + * Sender will be a user ID string + */ fun broadcastReceivedOpaque(senderId: String, payload: ByteArray) { val intent = Intent("$prefix.RECEIVED_OPAQUE") - intent.putExtra("$prefix.Sender", senderId) - intent.putExtra("$prefix.Payload", payload) + intent.putExtra(EXTRA_SENDER, senderId) + intent.putExtra(EXTRA_PAYLOAD, payload) sendBroadcast(intent) } fun broadcastNodeChange(nodeId: String, isOnline: Boolean) { val intent = Intent("$prefix.NODE_CHANGE") - intent.putExtra("$prefix.Id", nodeId) - intent.putExtra("$prefix.Online", isOnline) - sendBroadcast(intent) - } - - fun broadcastConnectionChanged(isConnected: Boolean) { - val intent = Intent("$prefix.CONNECTION_CHANGED") - intent.putExtra("$prefix.Connected", isConnected) + intent.putExtra(EXTRA_ID, nodeId) + intent.putExtra(EXTRA_ONLINE, isOnline) sendBroadcast(intent) } @@ -45,6 +48,30 @@ class MeshService : Service(), Logging { return binder } + override fun onCreate() { + super.onCreate() + + val filter = IntentFilter(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION) + registerReceiver(radioInterfaceReceiver, filter) + } + + override fun onDestroy() { + unregisterReceiver(radioInterfaceReceiver) + super.onDestroy() + } + + /** + * Receives messages from our BT radio service and processes them to update our model + * and send to clients as needed. + */ + private val radioInterfaceReceiver = object : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val proto = MeshProtos.FromRadio.parseFrom(intent.getByteArrayExtra(EXTRA_PAYLOAD)!!) + TODO("FIXME - update model and send messages as needed") + } + } + private val binder = object : IMeshService.Stub() { override fun setOwner(myId: String, longName: String, shortName: String) { error("TODO setOwner $myId : $longName : $shortName") diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index 116d370e8..5f418d76d 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -2,4 +2,7 @@ package com.geeksville.mesh import com.geeksville.android.GeeksvilleApplication -class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510baa46580e3") \ No newline at end of file +const val prefix = "com.geeksville.mesh" + +class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510baa46580e3") { +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt new file mode 100644 index 000000000..6257bb1ee --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt @@ -0,0 +1,93 @@ +package com.geeksville.mesh + +import android.content.Context +import android.content.Intent +import androidx.core.app.JobIntentService +import com.geeksville.android.Logging + +const val EXTRA_CONNECTED = "$prefix.Connected" +const val EXTRA_PAYLOAD = "$prefix.Payload" +const val EXTRA_SENDER = "$prefix.Sender" +const val EXTRA_ID = "$prefix.Id" +const val EXTRA_ONLINE = "$prefix.Online" + +/** + * Handles the bluetooth link with a mesh radio device. Does not cache any device state, + * just does bluetooth comms etc... + * + * This service is not exposed outside of this process. + * + * Note - this class intentionally dumb. It doesn't understand protobuf framing etc... + * It is designed to be simple so it can be stubbed out with a simulated version as needed. + */ +class RadioInterfaceService : JobIntentService(), Logging { + + companion object { + /** + * Unique job ID for this service. Must be the same for all work. + */ + private const val JOB_ID = 1001 + + /** + * The SEND_TORADIO + * Payload will be the raw bytes which were contained within a MeshProtos.ToRadio protobuf + */ + const val SEND_TORADIO_ACTION = "$prefix.SEND_TORADIO" + + /** + * The RECEIVED_FROMRADIO + * Payload will be the raw bytes which were contained within a MeshProtos.FromRadio protobuf + */ + const val RECEIVE_FROMRADIO_ACTION = "$prefix.RECEIVE_FROMRADIO" + + + /** + * Convenience method for enqueuing work in to this service. + */ + fun enqueueWork(context: Context, work: Intent) { + enqueueWork( + context, + RadioInterfaceService::class.java, JOB_ID, work + ) + } + + /// Helper function to send a packet to the radio + fun sendToRadio(context: Context, a: ByteArray) { + val i = Intent(SEND_TORADIO_ACTION) + i.putExtra(EXTRA_PAYLOAD, a) + enqueueWork(context, i) + } + } + + private fun broadcastReceivedFromRadio(payload: ByteArray) { + val intent = Intent(RECEIVE_FROMRADIO_ACTION) + intent.putExtra("$prefix.Payload", payload) + sendBroadcast(intent) + } + + fun broadcastConnectionChanged(isConnected: Boolean) { + val intent = Intent("$prefix.CONNECTION_CHANGED") + intent.putExtra(EXTRA_CONNECTED, isConnected) + sendBroadcast(intent) + } + + /// Send a packet/command out the radio link + private fun sendToRadio(p: ByteArray) { + info("Simulating sending to radio size=$p.size") + } + + // Handle an incoming packet from the radio, broadcasts it as an android intent + private fun handleFromRadio(p: ByteArray) { + broadcastReceivedFromRadio(p) + } + + override fun onHandleWork(intent: Intent) { // We have received work to do. The system or framework is already + // holding a wake lock for us at this point, so we can just go. + debug("Executing work: $intent") + when (intent.action) { + SEND_TORADIO_ACTION -> sendToRadio(intent.getByteArrayExtra(EXTRA_PAYLOAD)!!) + else -> TODO("Unhandled case") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/SoftwareUpdateService.kt index 7c823c68b..18568583a 100644 --- a/app/src/main/java/com/geeksville/mesh/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/SoftwareUpdateService.kt @@ -10,10 +10,7 @@ import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context import android.content.Intent -import android.os.Handler import android.os.ParcelUuid -import android.os.SystemClock -import android.widget.Toast import androidx.core.app.JobIntentService import com.geeksville.android.Logging import java.util.* @@ -184,19 +181,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { connectToTestDevice() // FIXME, pass in as an intent arg instead startUpdate() } - else -> logAssert(false) - } - - debug( - "Completed service @ " + SystemClock.elapsedRealtime() - ) - } - - val mHandler = Handler() - // Helper for showing tests - fun toast(text: CharSequence?) { - mHandler.post { - Toast.makeText(this@SoftwareUpdateService, text, Toast.LENGTH_SHORT).show() + else -> TODO("Unhandled case") } } @@ -204,10 +189,10 @@ class SoftwareUpdateService : JobIntentService(), Logging { /** * Unique job ID for this service. Must be the same for all work. */ - const val JOB_ID = 1000 + private const val JOB_ID = 1000 - val scanDevicesIntent = Intent("com.geeksville.com.geeeksville.mesh.SCAN_DEVICES") - val startUpdateIntent = Intent("com.geeksville.com.geeeksville.mesh.START_UPDATE") + val scanDevicesIntent = Intent("$prefix.SCAN_DEVICES") + val startUpdateIntent = Intent("$prefix.START_UPDATE") private const val SCAN_PERIOD: Long = 10000 diff --git a/app/src/main/proto/mesh.proto b/app/src/main/proto/mesh.proto index a9859d607..b0b3c380c 100644 --- a/app/src/main/proto/mesh.proto +++ b/app/src/main/proto/mesh.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package mesh; -option java_package = "com.geeksville.com.geeeksville.mesh"; +option java_package = "com.geeksville.mesh"; option java_outer_classname = "MeshProtos"; /** @@ -42,40 +42,40 @@ node number, or 0xff for broadcast. // a gps position message Position { - double latitude = 1; - double longitude = 2; - int32 altitude = 3; + double latitude = 1; + double longitude = 2; + int32 altitude = 3; } // Times are typically not sent over the mesh, but they will be added to any Packet (chain of SubPacket) // sent to the phone (so the phone can know exact time of reception) message Time { - uint64 msecs = 1; // msecs since 1970 + uint64 msecs = 1; // msecs since 1970 } // A message sent from a device outside of the mesh, in a form the mesh does not understand // i.e. a Signal app level message. message Opaque { - bytes payload = 1; + bytes payload = 1; } // a simple text message, which even the little micros in the mesh can understand and show on their screen message Text { - string text = 1; + string text = 1; } // Sent from the phone over bluetooth to set the user id for the owner of this node. // Also sent from nodes to each other when a new node signs on (so all clients can have this info) message User { - string id = 1; // a globally unique ID string for this user. In the case of Signal that would mean +16504442323 - string long_name = 2; // A full name for this user, i.e. "Kevin Hester" - string short_name = 3; // A VERY short name, ideally two characters. Suitable for a tiny OLED screen + string id = 1; // a globally unique ID string for this user. In the case of Signal that would mean +16504442323 + string long_name = 2; // A full name for this user, i.e. "Kevin Hester" + string short_name = 3; // A VERY short name, ideally two characters. Suitable for a tiny OLED screen } // Broadcast when a newly powered mesh node wants to find a node num it can use (see document for more // details) message WantNodeNum { - // No payload, just its existence is sufficent (desired node num will be in the from field) + // No payload, just its existence is sufficent (desired node num will be in the from field) } // Sent to a node which has requested a nodenum when it is told it can't have it @@ -84,35 +84,34 @@ message DenyNodeNum { // A single packet might have a series of SubPacket included message SubPacket { - oneof variant { - Position position = 1; - Time time = 2; - Text text = 3; - Opaque opaque = 4; - User user = 5; - WantNodeNum want_node = 6; - DenyNodeNum deny_node = 7; - } + oneof variant { + Position position = 1; + Time time = 2; + Text text = 3; + Opaque opaque = 4; + User user = 5; + WantNodeNum want_node = 6; + DenyNodeNum deny_node = 7; + } } - // A packet sent over our mesh. // NOTE: this raw payload does not include the from and to addresses, which are stripped off // and passed into the mesh library code separately. message MeshPayload { - repeated SubPacket subPackets = 3; + repeated SubPacket subPackets = 3; } // A full packet sent/received over the mesh message MeshPacket { - int32 from = 1; - int32 to = 2; - MeshPayload payload = 3; + int32 from = 1; + int32 to = 2; + MeshPayload payload = 3; } // Full settings (center freq, spread factor, preshared secret key etc...) needed to configure a radio message RadioConfig { - // FIXME + // FIXME } /** @@ -139,11 +138,11 @@ SET_CONFIG (switches device to a new set of radio params and preshared key, drop // Full information about a node on the mesh message NodeInfo { - int32 num = 1; // the node number - User user = 2; - int32 battery_level = 3; // 0-100 - Position position = 4; - Time last_seen = 5; + int32 num = 1; // the node number + User user = 2; + int32 battery_level = 3; // 0-100 + Position position = 4; + Time last_seen = 5; } // packets from the radio to the phone will appear on the fromRadio characteristic. It will support @@ -151,27 +150,27 @@ message NodeInfo { // it will sit in that descriptor until consumed by the phone, at which point the next item in the FIFO // will be populated. FIXME message FromRadio { - oneof variant { - MeshPacket packet = 1; - NodeInfo node_info = 2; - } + oneof variant { + MeshPacket packet = 1; + NodeInfo node_info = 2; + } } // packets/commands to the radio will be written (reliably) to the toRadio characteristic. Once the // write completes the phone can assume it is handled. message ToRadio { - // If sent to the radio, the radio will send the phone its full node DB (NodeInfo records) - // Used to populate network info the first time the phone connects to the radio - message WantNodes { - // Empty - } + // If sent to the radio, the radio will send the phone its full node DB (NodeInfo records) + // Used to populate network info the first time the phone connects to the radio + message WantNodes { + // Empty + } - oneof variant { - MeshPacket packet = 1; // send this packet on the mesh - RadioConfig set_radio = 2; // set the radio provisioning for this node - User set_owner = 3; // Set the owner for this node - WantNodes want_nodes = 4; // phone wants radio to send full node db to the phone - } + oneof variant { + MeshPacket packet = 1; // send this packet on the mesh + RadioConfig set_radio = 2; // set the radio provisioning for this node + User set_owner = 3; // Set the owner for this node + WantNodes want_nodes = 4; // phone wants radio to send full node db to the phone + } }