Skip to content
Snippets Groups Projects
Commit b10b6232 authored by Edmund Jochim's avatar Edmund Jochim :speech_balloon:
Browse files

New App update

parent 01d392a3
Branches
No related merge requests found
Showing
with 296 additions and 14 deletions
MecanumCarApp/App_Screenshot.jpg

36 KiB | W: | H:

MecanumCarApp/App_Screenshot.jpg

36.6 KiB | W: | H:

MecanumCarApp/App_Screenshot.jpg
MecanumCarApp/App_Screenshot.jpg
MecanumCarApp/App_Screenshot.jpg
MecanumCarApp/App_Screenshot.jpg
  • 2-up
  • Swipe
  • Onion skin
No preview for this file type
No preview for this file type
......@@ -12,6 +12,8 @@
android:supportsRtl="true"
android:theme="@style/Theme.MecanumCar">
<activity android:name=".SettingsActivity" />
<activity
android:name=".MainActivity"
android:exported="true">
......
package com.example.mecanumcar
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
......@@ -7,6 +8,9 @@ import android.view.View
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
......@@ -21,18 +25,22 @@ import kotlin.math.round
class MainActivity : AppCompatActivity() {
private val carIpAddress = "192.168.10.1" // Replace with your car's IP address
private val carPort = 4444
private var carIpAddress = "192.168.10.1" // Default IP address
private var carPort = 4444 // Default port
private var statusPort = 4445 // Default status port
private var speed = 64
private var currentDirection: String? = null
private val keepAliveInterval = 8000L // 8 seconds
private val maxRetryAttempts = 3 // Maximum number of retry attempts
private val handler = Handler(Looper.getMainLooper())
private lateinit var batteryInfoTextView: TextView
private lateinit var motorInfoTextView: TextView
private val keepAliveRunnable = object : Runnable {
override fun run() {
CoroutineScope(Dispatchers.Main).launch {
sendCommandWithRetry("keepalive")
sendCommand("keepalive")
//sendCommandWithRetry("keepalive")
}
handler.postDelayed(this, keepAliveInterval)
}
......@@ -42,7 +50,13 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sharedPrefs = getSharedPreferences("MecanumCarSettings", MODE_PRIVATE)
carIpAddress = sharedPrefs.getString("carIpAddress", carIpAddress) ?: carIpAddress
carPort = sharedPrefs.getString("carPort", carPort.toString())?.toInt() ?: carPort
statusPort = sharedPrefs.getString("statusPort", statusPort.toString())?.toInt() ?: statusPort
batteryInfoTextView = findViewById(R.id.batteryInfo)
motorInfoTextView = findViewById(R.id.motorInfo)
val buttons = mapOf(
"UP" to findViewById<Button>(R.id.btnUp),
......@@ -69,7 +83,8 @@ class MainActivity : AppCompatActivity() {
android.view.MotionEvent.ACTION_CANCEL -> {
currentDirection = null
CoroutineScope(Dispatchers.Main).launch {
sendCommandWithRetry("stop")
//sendCommandWithRetry("stop")
sendCommand("stop")
}
handler.removeCallbacks(keepAliveRunnable)
}
......@@ -89,14 +104,36 @@ class MainActivity : AppCompatActivity() {
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
val infoButton = findViewById<Button>(R.id.btnInfo)
infoButton.setOnClickListener {
showInfoDialog()
}
startListeningForCarStateUpdates()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.settings -> {
//Toast.makeText(this, "Settings Selected", Toast.LENGTH_SHORT).show()
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
return true
}
R.id.about -> {
//Toast.makeText(this, "About Selected", Toast.LENGTH_SHORT).show()
showInfoDialog() // Show the info dialog when "ABOUT" is selected
return true
}
R.id.exit -> {
//Toast.makeText(this, "Exit Selected", Toast.LENGTH_SHORT).show()
finish() // Close the app when "EXIT" is selected
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun sendDirectionalCommand(direction: String) {
val command = when (direction) {
"UP" -> "rc $speed 0 0"
......@@ -126,12 +163,17 @@ class MainActivity : AppCompatActivity() {
private suspend fun sendCommandWithRetry(command: String) {
var response: String?
var retryCount = 0 // Initialize retry count
do {
response = sendCommandAndWaitForResponse(command)
if (response == null || response == "error") {
delay(500) // Wait for 0.5 second before retrying
retryCount++
}
} while (response == null || response == "error")
} while ((response == null || response == "error") && retryCount < maxRetryAttempts)
if (retryCount == maxRetryAttempts) {
Toast.makeText(this, "Timeout", Toast.LENGTH_SHORT).show()
}
}
private suspend fun sendCommandAndWaitForResponse(command: String): String? {
......@@ -159,32 +201,55 @@ class MainActivity : AppCompatActivity() {
private fun startListeningForCarStateUpdates() {
CoroutineScope(Dispatchers.IO).launch {
val socket = DatagramSocket(4445)
val socket = DatagramSocket(statusPort)
val buffer = ByteArray(1024)
while (true) {
val packet = DatagramPacket(buffer, buffer.size)
socket.receive(packet)
val message = String(packet.data, 0, packet.length)
withContext(Dispatchers.Main) {
updateBatteryInfo(message)
//updateBatteryInfo(message)
//updateMotorInfo(message)
updateStatusInfo(message)
}
}
}
}
private fun updateStatusInfo(message: String) {
val regex = """batV:(\d+\.\d+),batP:(\d+),mA:(\d+),mB:(\d+),mC:(\d+),mD:(\d+),""".toRegex()
val matchResult = regex.find(message)
if (matchResult != null) {
val (voltage, percentage, mA, mB, mC, mD) = matchResult.destructured
batteryInfoTextView.text = "$voltage V \n$percentage %"
motorInfoTextView.text = "MotorA: $mA % | MotorB: $mB %\nMotorC: $mC % | MotorD: $mD %"
}
}
/*
private fun updateBatteryInfo(message: String) {
val regex = """batV:(\d+\.\d+),batP:(\d+),""".toRegex()
val matchResult = regex.find(message)
if (matchResult != null) {
val (voltage, percentage) = matchResult.destructured
batteryInfoTextView.text = "$voltage V | $percentage %"
batteryInfoTextView.text = "$voltage V \n$percentage %"
}
}
private fun updateMotorInfo(message: String) {
val regex = """mA:(\d+),mB:(\d+),mC:(\d+),mD:(\d+)""".toRegex()
val matchResult = regex.find(message)
if (matchResult != null) {
val (mA, mB, mC, mD) = matchResult.destructured
motorInfoTextView.text = "$mA % | $mB % | $mC % | $mD %"
}
}
*/
private fun showInfoDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle("Info")
builder.setMessage("Mecanum Wheel Car Wifi Control v1.3\nAuthor: Edmund Jochim\nTHM CCCE Case Study 2024\nLicense: GNU GPLv3")
builder.setMessage("Mecanum Wheel Car Wifi Control v1.4\nAuthor: Edmund Jochim\nTHM CCCE Case Study 2024\nLicense:\nApp: GNU GPLv3\nIcons: Apache-2.0\n\n" +
"THM Logo is the property of Technische Hochschule Mittelhessen")
builder.setPositiveButton("Close") { dialog, _ ->
dialog.dismiss()
}
......
package com.example.mecanumcar
import android.os.Bundle
import android.text.InputFilter
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val ipField1 = findViewById<EditText>(R.id.ipField1)
val ipField2 = findViewById<EditText>(R.id.ipField2)
val ipField3 = findViewById<EditText>(R.id.ipField3)
val ipField4 = findViewById<EditText>(R.id.ipField4)
val portField = findViewById<EditText>(R.id.portField)
val statusPortField = findViewById<EditText>(R.id.statusPortField)
val saveButton = findViewById<Button>(R.id.saveButton)
val exitButton = findViewById<Button>(R.id.exitButton)
// Set input filters
val ipFilter = InputFilter { source, _, _, _, _, _ ->
try {
val input = source.toString().toInt()
if (input in 0..255) source else ""
} catch (e: NumberFormatException) {
""
}
}
ipField1.filters = arrayOf(ipFilter)
ipField2.filters = arrayOf(ipFilter)
ipField3.filters = arrayOf(ipFilter)
ipField4.filters = arrayOf(ipFilter)
val portFilter = InputFilter { source, _, _, _, _, _ ->
try {
val input = source.toString().toInt()
if (input in 1025..65535) source else ""
} catch (e: NumberFormatException) {
""
}
}
portField.filters = arrayOf(portFilter)
statusPortField.filters = arrayOf(portFilter)
// Load existing settings
val sharedPrefs = getSharedPreferences("MecanumCarSettings", MODE_PRIVATE)
ipField1.setText(sharedPrefs.getString("carIpField1", "192"))
ipField2.setText(sharedPrefs.getString("carIpField2", "168"))
ipField3.setText(sharedPrefs.getString("carIpField3", "10"))
ipField4.setText(sharedPrefs.getString("carIpField4", "1"))
portField.setText(sharedPrefs.getString("carPort", "4444"))
statusPortField.setText(sharedPrefs.getString("statusPort", "4445"))
// Save settings on button click
saveButton.setOnClickListener {
val newIp = "${ipField1.text}.${ipField2.text}.${ipField3.text}.${ipField4.text}"
val newPort = portField.text.toString()
val newStatusPort = statusPortField.text.toString()
if (ipField1.text.isBlank() || ipField2.text.isBlank() || ipField3.text.isBlank() || ipField4.text.isBlank() ||
newPort.isBlank() || newStatusPort.isBlank()) {
Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val editor = sharedPrefs.edit()
editor.putString("carIpField1", ipField1.text.toString())
editor.putString("carIpField2", ipField2.text.toString())
editor.putString("carIpField3", ipField3.text.toString())
editor.putString("carIpField4", ipField4.text.toString())
editor.putString("carIpAddress", newIp)
editor.putString("carPort", newPort)
editor.putString("statusPort", newStatusPort)
editor.apply()
Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show()
finish()
}
// Exit without saving on button click
exitButton.setOnClickListener {
finish()
}
}
}
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/button_normal" />
</selector>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#80BA24" /> <!-- Normal color -->
<corners android:radius="8dp" />
<padding android:left="16dp" android:top="16dp" android:right="16dp" android:bottom="16dp" />
</shape>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#5a821b" /> <!-- Pressed color -->
<corners android:radius="8dp" />
<padding android:left="16dp" android:top="16dp" android:right="16dp" android:bottom="16dp" />
</shape>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M560,720L320,480L560,240L616,296L432,480L616,664L560,720Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,800L600,800L600,240L360,240L360,800Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,720L600,720L600,240L360,240L360,720Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,640L600,640L600,240L360,240L360,640Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,560L600,560L600,240L360,240L360,560Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,480L600,480L600,240L360,240L360,480Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,400L600,400L600,240L360,240L360,400Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880ZM360,320L600,320L600,240L360,240L360,320Z"/>
</vector>
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment