diff --git a/mesh_service_example/src/androidTest/java/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.java b/mesh_service_example/src/androidTest/java/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.java
deleted file mode 100644
index d895cb7c1..000000000
--- a/mesh_service_example/src/androidTest/java/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.meshtastic.android.meshserviceexample;
-
-import android.content.Context;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
- assertEquals("com.meshtastic.android.meshserviceexample", appContext.getPackageName());
- }
-}
\ No newline at end of file
diff --git a/mesh_service_example/src/androidTest/kotlin/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.kt b/mesh_service_example/src/androidTest/kotlin/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..3208e2828
--- /dev/null
+++ b/mesh_service_example/src/androidTest/kotlin/com/meshtastic/android/meshserviceexample/ExampleInstrumentedTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.meshtastic.android.meshserviceexample
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ Assert.assertEquals("com.meshtastic.android.meshserviceexample", appContext.packageName)
+ }
+}
diff --git a/mesh_service_example/src/main/java/com/meshtastic/android/meshserviceexample/MainActivity.java b/mesh_service_example/src/main/java/com/meshtastic/android/meshserviceexample/MainActivity.java
deleted file mode 100644
index b0b41d616..000000000
--- a/mesh_service_example/src/main/java/com/meshtastic/android/meshserviceexample/MainActivity.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.meshtastic.android.meshserviceexample;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.activity.EdgeToEdge;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-
-import com.geeksville.mesh.DataPacket;
-import com.geeksville.mesh.IMeshService;
-import com.geeksville.mesh.MessageStatus;
-import com.geeksville.mesh.NodeInfo;
-import com.geeksville.mesh.Portnums;
-
-import java.util.Objects;
-
-public class MainActivity extends AppCompatActivity {
-
- private static final String TAG = "MeshServiceExample";
- private IMeshService meshService;
- private ServiceConnection serviceConnection;
- private boolean isMeshServiceBound = false;
-
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- EdgeToEdge.enable(this);
- setContentView(R.layout.activity_main);
- ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
- Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
- v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
- return insets;
- });
-
- TextView mainTextView = findViewById(R.id.mainTextView);
- ImageView statusImageView = findViewById(R.id.statusImageView);
- Button sendButton = findViewById(R.id.sendBtn);
-
- sendButton.setOnClickListener(v -> {
- if (meshService != null) {
- try {
- byte[] bytes = "Hello from MeshServiceExample".getBytes();
- DataPacket dataPacket = new DataPacket(DataPacket.ID_BROADCAST, bytes, Portnums.PortNum.TEXT_MESSAGE_APP_VALUE, DataPacket.ID_LOCAL, System.currentTimeMillis(), 0, MessageStatus.UNKNOWN, 3, 0, true);
- meshService.send(dataPacket);
- Log.d(TAG, "Message sent successfully");
- } catch (Exception e) {
- Log.e(TAG, "Failed to send message", e);
- }
- } else {
- Log.w(TAG, "MeshService is not bound, cannot send message");
- }
- });
-
- // Now you can call methods on meshService
- serviceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- meshService = IMeshService.Stub.asInterface(service);
- Log.i(TAG, "Connected to MeshService");
- isMeshServiceBound = true;
- statusImageView.setImageResource(android.R.color.holo_green_light);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- meshService = null;
- isMeshServiceBound = false;
- }
- };
-
- // Handle the received broadcast
- // handle node changed
- // handle position app data
- BroadcastReceiver meshtasticReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent == null || intent.getAction() == null) {
- Log.w(TAG, "Received null intent or action");
- return;
- }
- // Handle the received broadcast
- String action = intent.getAction();
- Log.d(TAG, "Received broadcast: " + action);
-
- switch (Objects.requireNonNull(action)) {
- case "com.geeksville.mesh.NODE_CHANGE":
- // handle node changed
- try {
- NodeInfo ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo");
- Log.d(TAG, "NodeInfo: " + ni);
- mainTextView.setText("NodeInfo: " + ni);
- } catch (Exception e) {
- Log.e(TAG, "onReceive: " + e.getMessage());
- return;
- }
- break;
- case "com.geeksville.mesh.MESSAGE_STATUS":
- int id = intent.getIntExtra("com.geeksville.mesh.PacketId", 0);
- MessageStatus status = intent.getParcelableExtra("com.geeksville.mesh.Status");
- Log.d(TAG, "Message Status ID: " + id + " Status: " + status);
- break;
- case "com.geeksville.mesh.MESH_CONNECTED": {
- String extraConnected = intent.getStringExtra("com.geeksville.mesh.Connected");
- boolean connected = extraConnected.equalsIgnoreCase("connected");
- Log.d(TAG, "Received ACTION_MESH_CONNECTED: " + extraConnected);
- if (connected) {
- statusImageView.setImageResource(android.R.color.holo_green_light);
- }
- break;
- }
- case "com.geeksville.mesh.MESH_DISCONNECTED": {
- String extraConnected = intent.getStringExtra("com.geeksville.mesh.Disconnected");
- boolean disconnected = extraConnected.equalsIgnoreCase("disconnected");
- Log.d(TAG, "Received ACTION_MESH_DISTCONNECTED: " + extraConnected);
- if (disconnected) {
- statusImageView.setImageResource(android.R.color.holo_red_light);
- }
- break;
- }
- case "com.geeksville.mesh.RECEIVED.POSITION_APP": {
- // handle position app data
- try {
- NodeInfo ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo");
- Log.d(TAG, "Position App NodeInfo: " + ni);
- mainTextView.setText("Position App NodeInfo: " + ni);
- } catch (Exception e) {
- e.printStackTrace();
- return;
- }
- break;
- }
- default:
- Log.w(TAG, "Unknown action: " + action);
- }
- }
- };
-
- IntentFilter filter = new IntentFilter();
- filter.addAction("com.geeksville.mesh.NODE_CHANGE");
- filter.addAction("com.geeksville.mesh.RECEIVED.NODEINFO_APP");
- filter.addAction("com.geeksville.mesh.RECEIVED.POSITION_APP");
- filter.addAction("com.geeksville.mesh.MESH_CONNECTED");
- filter.addAction("com.geeksville.mesh.MESH_DISCONNECTED");
- registerReceiver(meshtasticReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
- Log.d(TAG, "Registered meshtasticPacketReceiver");
-
- while (!bindMeshService()) {
- try {
- // Wait for the service to bind
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- Log.e(TAG, "Binding interrupted", e);
- break;
- }
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unbindMeshService();
- }
-
- private boolean bindMeshService() {
- try {
- Log.i(TAG, "Attempting to bind to Mesh Service...");
- Intent intent = new Intent("com.geeksville.mesh.Service");
- intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService");
- return bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
- } catch (Exception e) {
- Log.e(TAG, "Failed to bind", e);
- }
- return false;
- }
-
- private void unbindMeshService() {
- if (isMeshServiceBound) {
- try {
- unbindService(serviceConnection);
- } catch (IllegalArgumentException e) {
- Log.w(TAG, "MeshService not registered or already unbound: " + e.getMessage());
- }
- isMeshServiceBound = false;
- meshService = null;
- }
- }
-
-}
\ No newline at end of file
diff --git a/mesh_service_example/src/main/java/com/geeksville/mesh/DataPacket.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt
similarity index 100%
rename from mesh_service_example/src/main/java/com/geeksville/mesh/DataPacket.kt
rename to mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt
diff --git a/mesh_service_example/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt
similarity index 100%
rename from mesh_service_example/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
rename to mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt
diff --git a/mesh_service_example/src/main/java/com/geeksville/mesh/NodeInfo.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt
similarity index 100%
rename from mesh_service_example/src/main/java/com/geeksville/mesh/NodeInfo.kt
rename to mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt
diff --git a/mesh_service_example/src/main/java/com/geeksville/mesh/util/Extensions.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt
similarity index 100%
rename from mesh_service_example/src/main/java/com/geeksville/mesh/util/Extensions.kt
rename to mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt
diff --git a/mesh_service_example/src/main/java/com/geeksville/mesh/util/LocationUtils.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt
similarity index 100%
rename from mesh_service_example/src/main/java/com/geeksville/mesh/util/LocationUtils.kt
rename to mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt
diff --git a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
new file mode 100644
index 000000000..79b8164f8
--- /dev/null
+++ b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.meshtastic.android.meshserviceexample
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.activity.enableEdgeToEdge
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import com.geeksville.mesh.DataPacket
+import com.geeksville.mesh.IMeshService
+import com.geeksville.mesh.MessageStatus
+import com.geeksville.mesh.NodeInfo
+import com.geeksville.mesh.Portnums
+
+private const val TAG: String = "MeshServiceExample"
+
+class MainActivity : AppCompatActivity() {
+ private var meshService: IMeshService? = null
+ private var serviceConnection: ServiceConnection? = null
+ private var isMeshServiceBound = false
+
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ this.enableEdgeToEdge()
+ setContentView(R.layout.activity_main)
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ insets
+ }
+
+ val mainTextView = findViewById(R.id.mainTextView)
+ val statusImageView = findViewById(R.id.statusImageView)
+
+ findViewById