Compare commits
15 Commits
efa93ab912
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| 682af406f5 | |||
| 0557622dc3 | |||
| 59eb3c7440 | |||
| d2f8d0ff06 | |||
| 3e1c5edc19 | |||
| 027116e18e | |||
| c8abfd94c3 | |||
| 0763c2e1df | |||
| 3ac39136c6 | |||
| 5e0f2f1bb7 | |||
| 3517414ec1 | |||
| 3b62743481 | |||
| 18bd21fba1 | |||
| 77a3b19b24 | |||
| 70cd547fb7 |
10
.idea/deploymentTargetDropDown.xml
generated
Normal file
10
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<value>
|
||||||
|
<entry key="app">
|
||||||
|
<State />
|
||||||
|
</entry>
|
||||||
|
</value>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
5
.idea/gradle.xml
generated
5
.idea/gradle.xml
generated
@@ -4,16 +4,15 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -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="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.6.10" />
|
<option name="version" value="1.8.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
10
.idea/migrations.xml
generated
Normal file
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("com.google.devtools.ksp").version("1.6.10-1.0.4")
|
|
||||||
id("org.jlleitschuh.gradle.ktlint").version("12.0.3")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -65,6 +63,7 @@ dependencies {
|
|||||||
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
|
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
|
||||||
implementation("androidx.navigation:navigation-compose:2.6.0")
|
implementation("androidx.navigation:navigation-compose:2.6.0")
|
||||||
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
|
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
|
||||||
|
implementation("com.github.manalkaff:JetStick:1.2")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.helible.pilot
|
package com.helible.pilot
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,18 +3,34 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
|
android:name="android.permission.BLUETOOTH"
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
|
android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/>
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
<!-- Request Bluetooth permissions for API level 31+ -->
|
<!-- Request Bluetooth permissions for API level 31+ -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:minSdkVersion="31"
|
<uses-permission
|
||||||
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:minSdkVersion="31"
|
android:minSdkVersion="31"
|
||||||
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
tools:targetApi="s" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_CONNECT"
|
||||||
|
android:minSdkVersion="31"
|
||||||
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
tools:targetApi="s" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
<uses-feature
|
||||||
|
android:name="android.hardware.bluetooth"
|
||||||
|
android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -24,12 +40,12 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Testblue"
|
android:theme="@style/Theme.Main"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name="com.helible.pilot.MainActivity"
|
android:name="com.helible.pilot.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.Testblue">
|
android:theme="@style/Theme.Main">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import com.curiouscreature.kotlin.math.*
|
||||||
|
|
||||||
|
data class Camera(
|
||||||
|
inline var position: Float3 = Float3(),
|
||||||
|
inline var target: Float3 = Float3()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Mesh(val name: String, val triangles: Array<Mat3>) {
|
||||||
|
var position = Float3()
|
||||||
|
var rotation = Float3()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cube = Mesh(
|
||||||
|
"Cube", arrayOf(
|
||||||
|
|
||||||
|
// SOUTH
|
||||||
|
Mat3(Float3(-1.0f, -1.0f, -1.0f), Float3(-1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f)),
|
||||||
|
Mat3(Float3(-1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f), Float3(1.0f, -1.0f, -1.0f)),
|
||||||
|
|
||||||
|
// EAST
|
||||||
|
Mat3(Float3(1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f)),
|
||||||
|
Mat3(Float3(1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(1.0f, -1.0f, 1.0f)),
|
||||||
|
|
||||||
|
// NORTH
|
||||||
|
Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f)),
|
||||||
|
Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(-1.0f, -1.0f, 1.0f)),
|
||||||
|
|
||||||
|
// WEST
|
||||||
|
Mat3(Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(-1.0f, 1.0f, -1.0f)),
|
||||||
|
Mat3(Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, -1.0f), Float3(-1.0f, -1.0f, -1.0f)),
|
||||||
|
|
||||||
|
// TOP
|
||||||
|
Mat3(Float3(-1.0f, 1.0f, -1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(1.0f, 1.0f, 1.0f)),
|
||||||
|
Mat3(Float3(-1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(1.0f, 1.0f, -1.0f)),
|
||||||
|
|
||||||
|
// BOTTOM
|
||||||
|
Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, -1.0f)),
|
||||||
|
Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, -1.0f), Float3(1.0f, -1.0f, -1.0f)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun project(coord: Float3, transMat: Mat4): Float2 {
|
||||||
|
val (x, y, _, _) = transMat * (Float4(coord, 0f))
|
||||||
|
return Float2(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project triangle to 2d
|
||||||
|
* this was a shot in the dark, might be a bug here. Looks ok
|
||||||
|
*/
|
||||||
|
fun Mat3.project(transMat: Mat4): List<Float2> {
|
||||||
|
|
||||||
|
val projected = transMat * Mat4(this.x, this.y, this.z)
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
Float2(projected[0][0], projected[0][1]),
|
||||||
|
Float2(projected[1][0], projected[1][1]),
|
||||||
|
Float2(projected[2][0], projected[2][1]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render3d(camera: Camera, mesh: Mesh): List<List<Float2>> {
|
||||||
|
|
||||||
|
val viewMatrix = lookAt(camera.position, camera.target)
|
||||||
|
val projectionMatrix = perspective(0.78f, 1f, 0.01f, 20f)
|
||||||
|
val worldMatrix = rotation(mesh.rotation) * translation(mesh.position)
|
||||||
|
val transformMatrix = projectionMatrix * viewMatrix * worldMatrix
|
||||||
|
return mesh.triangles.map { it.project(transformMatrix) }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Romain Guy
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.curiouscreature.kotlin.math
|
||||||
|
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
enum class MatrixColumn {
|
||||||
|
X, Y, Z, W
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Mat2(
|
||||||
|
var x: Float2 = Float2(x = 1.0f),
|
||||||
|
var y: Float2 = Float2(y = 1.0f)) {
|
||||||
|
constructor(m: Mat2) : this(m.x.copy(), m.y.copy())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun of(vararg a: Float): Mat2 {
|
||||||
|
require(a.size >= 4)
|
||||||
|
return Mat2(
|
||||||
|
Float2(a[0], a[2]),
|
||||||
|
Float2(a[1], a[3])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identity() = Mat2()
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(column: Int) = when(column) {
|
||||||
|
0 -> x
|
||||||
|
1 -> y
|
||||||
|
else -> throw IllegalArgumentException("column must be in 0..1")
|
||||||
|
}
|
||||||
|
operator fun get(column: Int, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun get(column: MatrixColumn) = when(column) {
|
||||||
|
MatrixColumn.X -> x
|
||||||
|
MatrixColumn.Y -> y
|
||||||
|
else -> throw IllegalArgumentException("column must be X or Y")
|
||||||
|
}
|
||||||
|
operator fun get(column: MatrixColumn, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1]
|
||||||
|
operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v)
|
||||||
|
|
||||||
|
operator fun set(column: Int, v: Float2) {
|
||||||
|
this[column].xy = v
|
||||||
|
}
|
||||||
|
operator fun set(column: Int, row: Int, v: Float) {
|
||||||
|
this[column][row] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun unaryMinus() = Mat2(-x, -y)
|
||||||
|
operator fun inc(): Mat2 {
|
||||||
|
x++
|
||||||
|
y++
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
operator fun dec(): Mat2 {
|
||||||
|
x--
|
||||||
|
y--
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(v: Float) = Mat2(x + v, y + v)
|
||||||
|
operator fun minus(v: Float) = Mat2(x - v, y - v)
|
||||||
|
operator fun times(v: Float) = Mat2(x * v, y * v)
|
||||||
|
operator fun div(v: Float) = Mat2(x / v, y / v)
|
||||||
|
|
||||||
|
operator fun times(m: Mat2): Mat2 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Mat2(
|
||||||
|
Float2(dot(t.x, m.x), dot(t.y, m.x)),
|
||||||
|
Float2(dot(t.x, m.y), dot(t.y, m.y))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun times(v: Float2): Float2 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Float2(dot(t.x, v), dot(t.y, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toFloatArray() = floatArrayOf(
|
||||||
|
x.x, y.x,
|
||||||
|
x.y, y.y
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return """
|
||||||
|
|${x.x} ${y.x}|
|
||||||
|
|${x.y} ${y.y}|
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Mat3(
|
||||||
|
var x: Float3 = Float3(x = 1.0f),
|
||||||
|
var y: Float3 = Float3(y = 1.0f),
|
||||||
|
var z: Float3 = Float3(z = 1.0f)) {
|
||||||
|
constructor(m: Mat3) : this(m.x.copy(), m.y.copy(), m.z.copy())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun of(vararg a: Float): Mat3 {
|
||||||
|
require(a.size >= 9)
|
||||||
|
return Mat3(
|
||||||
|
Float3(a[0], a[3], a[6]),
|
||||||
|
Float3(a[1], a[4], a[7]),
|
||||||
|
Float3(a[2], a[5], a[8])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identity() = Mat3()
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(column: Int) = when(column) {
|
||||||
|
0 -> x
|
||||||
|
1 -> y
|
||||||
|
2 -> z
|
||||||
|
else -> throw IllegalArgumentException("column must be in 0..2")
|
||||||
|
}
|
||||||
|
operator fun get(column: Int, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun get(column: MatrixColumn) = when(column) {
|
||||||
|
MatrixColumn.X -> x
|
||||||
|
MatrixColumn.Y -> y
|
||||||
|
MatrixColumn.Z -> z
|
||||||
|
else -> throw IllegalArgumentException("column must be X, Y or Z")
|
||||||
|
}
|
||||||
|
operator fun get(column: MatrixColumn, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1]
|
||||||
|
operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v)
|
||||||
|
|
||||||
|
operator fun set(column: Int, v: Float3) {
|
||||||
|
this[column].xyz = v
|
||||||
|
}
|
||||||
|
operator fun set(column: Int, row: Int, v: Float) {
|
||||||
|
this[column][row] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun unaryMinus() = Mat3(-x, -y, -z)
|
||||||
|
operator fun inc(): Mat3 {
|
||||||
|
x++
|
||||||
|
y++
|
||||||
|
z++
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
operator fun dec(): Mat3 {
|
||||||
|
x--
|
||||||
|
y--
|
||||||
|
z--
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(v: Float) = Mat3(x + v, y + v, z + v)
|
||||||
|
operator fun minus(v: Float) = Mat3(x - v, y - v, z - v)
|
||||||
|
operator fun times(v: Float) = Mat3(x * v, y * v, z * v)
|
||||||
|
operator fun div(v: Float) = Mat3(x / v, y / v, z / v)
|
||||||
|
|
||||||
|
operator fun times(m: Mat3): Mat3 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Mat3(
|
||||||
|
Float3(dot(t.x, m.x), dot(t.y, m.x), dot(t.z, m.x)),
|
||||||
|
Float3(dot(t.x, m.y), dot(t.y, m.y), dot(t.z, m.y)),
|
||||||
|
Float3(dot(t.x, m.z), dot(t.y, m.z), dot(t.z, m.z))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun times(v: Float3): Float3 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Float3(dot(t.x, v), dot(t.y, v), dot(t.z, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toFloatArray() = floatArrayOf(
|
||||||
|
x.x, y.x, z.x,
|
||||||
|
x.y, y.y, z.y,
|
||||||
|
x.z, y.z, z.z
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return """
|
||||||
|
|${x.x} ${y.x} ${z.x}|
|
||||||
|
|${x.y} ${y.y} ${z.y}|
|
||||||
|
|${x.z} ${y.z} ${z.z}|
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Mat4(
|
||||||
|
var x: Float4 = Float4(x = 1.0f),
|
||||||
|
var y: Float4 = Float4(y = 1.0f),
|
||||||
|
var z: Float4 = Float4(z = 1.0f),
|
||||||
|
var w: Float4 = Float4(w = 1.0f)) {
|
||||||
|
constructor(right: Float3, up: Float3, forward: Float3, position: Float3 = Float3()) :
|
||||||
|
this(Float4(right), Float4(up), Float4(forward), Float4(position, 1.0f))
|
||||||
|
constructor(m: Mat4) : this(m.x.copy(), m.y.copy(), m.z.copy(), m.w.copy())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun of(vararg a: Float): Mat4 {
|
||||||
|
require(a.size >= 16)
|
||||||
|
return Mat4(
|
||||||
|
Float4(a[0], a[4], a[8], a[12]),
|
||||||
|
Float4(a[1], a[5], a[9], a[13]),
|
||||||
|
Float4(a[2], a[6], a[10], a[14]),
|
||||||
|
Float4(a[3], a[7], a[11], a[15])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identity() = Mat4()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline var right: Float3
|
||||||
|
get() = x.xyz
|
||||||
|
set(value) {
|
||||||
|
x.xyz = value
|
||||||
|
}
|
||||||
|
inline var up: Float3
|
||||||
|
get() = y.xyz
|
||||||
|
set(value) {
|
||||||
|
y.xyz = value
|
||||||
|
}
|
||||||
|
inline var forward: Float3
|
||||||
|
get() = z.xyz
|
||||||
|
set(value) {
|
||||||
|
z.xyz = value
|
||||||
|
}
|
||||||
|
inline var position: Float3
|
||||||
|
get() = w.xyz
|
||||||
|
set(value) {
|
||||||
|
w.xyz = value
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val scale: Float3
|
||||||
|
get() = Float3(length(x.xyz), length(y.xyz), length(z.xyz))
|
||||||
|
inline val translation: Float3
|
||||||
|
get() = w.xyz
|
||||||
|
val rotation: Float3
|
||||||
|
get() {
|
||||||
|
val x = normalize(right)
|
||||||
|
val y = normalize(up)
|
||||||
|
val z = normalize(forward)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
z.y <= -1.0f -> Float3(degrees(-HALF_PI), 0.0f, degrees(atan2( x.z, y.z)))
|
||||||
|
z.y >= 1.0f -> Float3(degrees( HALF_PI), 0.0f, degrees(atan2(-x.z, -y.z)))
|
||||||
|
else -> Float3(
|
||||||
|
degrees(-asin(z.y)), degrees(-atan2(z.x, z.z)), degrees(atan2( x.y, y.y)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val upperLeft: Mat3
|
||||||
|
get() = Mat3(x.xyz, y.xyz, z.xyz)
|
||||||
|
|
||||||
|
operator fun get(column: Int) = when(column) {
|
||||||
|
0 -> x
|
||||||
|
1 -> y
|
||||||
|
2 -> z
|
||||||
|
3 -> w
|
||||||
|
else -> throw IllegalArgumentException("column must be in 0..3")
|
||||||
|
}
|
||||||
|
operator fun get(column: Int, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun get(column: MatrixColumn) = when(column) {
|
||||||
|
MatrixColumn.X -> x
|
||||||
|
MatrixColumn.Y -> y
|
||||||
|
MatrixColumn.Z -> z
|
||||||
|
MatrixColumn.W -> w
|
||||||
|
}
|
||||||
|
operator fun get(column: MatrixColumn, row: Int) = get(column)[row]
|
||||||
|
|
||||||
|
operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1]
|
||||||
|
operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v)
|
||||||
|
|
||||||
|
operator fun set(column: Int, v: Float4) {
|
||||||
|
this[column].xyzw = v
|
||||||
|
}
|
||||||
|
operator fun set(column: Int, row: Int, v: Float) {
|
||||||
|
this[column][row] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun unaryMinus() = Mat4(-x, -y, -z, -w)
|
||||||
|
operator fun inc(): Mat4 {
|
||||||
|
x++
|
||||||
|
y++
|
||||||
|
z++
|
||||||
|
w++
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
operator fun dec(): Mat4 {
|
||||||
|
x--
|
||||||
|
y--
|
||||||
|
z--
|
||||||
|
w--
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(v: Float) = Mat4(x + v, y + v, z + v, w + v)
|
||||||
|
operator fun minus(v: Float) = Mat4(x - v, y - v, z - v, w - v)
|
||||||
|
operator fun times(v: Float) = Mat4(x * v, y * v, z * v, w * v)
|
||||||
|
operator fun div(v: Float) = Mat4(x / v, y / v, z / v, w / v)
|
||||||
|
|
||||||
|
operator fun times(m: Mat4): Mat4 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Mat4(
|
||||||
|
Float4(dot(t.x, m.x), dot(t.y, m.x), dot(t.z, m.x), dot(t.w, m.x)),
|
||||||
|
Float4(dot(t.x, m.y), dot(t.y, m.y), dot(t.z, m.y), dot(t.w, m.y)),
|
||||||
|
Float4(dot(t.x, m.z), dot(t.y, m.z), dot(t.z, m.z), dot(t.w, m.z)),
|
||||||
|
Float4(dot(t.x, m.w), dot(t.y, m.w), dot(t.z, m.w), dot(t.w, m.w))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun times(v: Float4): Float4 {
|
||||||
|
val t = transpose(this)
|
||||||
|
return Float4(dot(t.x, v), dot(t.y, v), dot(t.z, v), dot(t.w, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toFloatArray() = floatArrayOf(
|
||||||
|
x.x, y.x, z.x, w.x,
|
||||||
|
x.y, y.y, z.y, w.y,
|
||||||
|
x.z, y.z, z.z, w.z,
|
||||||
|
x.w, y.w, z.w, w.w
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return """
|
||||||
|
|${x.x} ${y.x} ${z.x} ${w.x}|
|
||||||
|
|${x.y} ${y.y} ${z.y} ${w.y}|
|
||||||
|
|${x.z} ${y.z} ${z.z} ${w.z}|
|
||||||
|
|${x.w} ${y.w} ${z.w} ${w.w}|
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transpose(m: Mat2) = Mat2(
|
||||||
|
Float2(m.x.x, m.y.x),
|
||||||
|
Float2(m.x.y, m.y.y)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun transpose(m: Mat3) = Mat3(
|
||||||
|
Float3(m.x.x, m.y.x, m.z.x),
|
||||||
|
Float3(m.x.y, m.y.y, m.z.y),
|
||||||
|
Float3(m.x.z, m.y.z, m.z.z)
|
||||||
|
)
|
||||||
|
fun inverse(m: Mat3): Mat3 {
|
||||||
|
val a = m.x.x
|
||||||
|
val b = m.x.y
|
||||||
|
val c = m.x.z
|
||||||
|
val d = m.y.x
|
||||||
|
val e = m.y.y
|
||||||
|
val f = m.y.z
|
||||||
|
val g = m.z.x
|
||||||
|
val h = m.z.y
|
||||||
|
val i = m.z.z
|
||||||
|
|
||||||
|
val A = e * i - f * h
|
||||||
|
val B = f * g - d * i
|
||||||
|
val C = d * h - e * g
|
||||||
|
|
||||||
|
val det = a * A + b * B + c * C
|
||||||
|
|
||||||
|
return Mat3.of(
|
||||||
|
A / det, B / det, C / det,
|
||||||
|
(c * h - b * i) / det, (a * i - c * g) / det, (b * g - a * h) / det,
|
||||||
|
(b * f - c * e) / det, (c * d - a * f) / det, (a * e - b * d) / det
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transpose(m: Mat4) = Mat4(
|
||||||
|
Float4(m.x.x, m.y.x, m.z.x, m.w.x),
|
||||||
|
Float4(m.x.y, m.y.y, m.z.y, m.w.y),
|
||||||
|
Float4(m.x.z, m.y.z, m.z.z, m.w.z),
|
||||||
|
Float4(m.x.w, m.y.w, m.z.w, m.w.w)
|
||||||
|
)
|
||||||
|
fun inverse(m: Mat4): Mat4 {
|
||||||
|
val result = Mat4()
|
||||||
|
|
||||||
|
var pair0 = m.z.z * m.w.w
|
||||||
|
var pair1 = m.w.z * m.z.w
|
||||||
|
var pair2 = m.y.z * m.w.w
|
||||||
|
var pair3 = m.w.z * m.y.w
|
||||||
|
var pair4 = m.y.z * m.z.w
|
||||||
|
var pair5 = m.z.z * m.y.w
|
||||||
|
var pair6 = m.x.z * m.w.w
|
||||||
|
var pair7 = m.w.z * m.x.w
|
||||||
|
var pair8 = m.x.z * m.z.w
|
||||||
|
var pair9 = m.z.z * m.x.w
|
||||||
|
var pair10 = m.x.z * m.y.w
|
||||||
|
var pair11 = m.y.z * m.x.w
|
||||||
|
|
||||||
|
result.x.x = pair0 * m.y.y + pair3 * m.z.y + pair4 * m.w.y
|
||||||
|
result.x.x -= pair1 * m.y.y + pair2 * m.z.y + pair5 * m.w.y
|
||||||
|
result.x.y = pair1 * m.x.y + pair6 * m.z.y + pair9 * m.w.y
|
||||||
|
result.x.y -= pair0 * m.x.y + pair7 * m.z.y + pair8 * m.w.y
|
||||||
|
result.x.z = pair2 * m.x.y + pair7 * m.y.y + pair10 * m.w.y
|
||||||
|
result.x.z -= pair3 * m.x.y + pair6 * m.y.y + pair11 * m.w.y
|
||||||
|
result.x.w = pair5 * m.x.y + pair8 * m.y.y + pair11 * m.z.y
|
||||||
|
result.x.w -= pair4 * m.x.y + pair9 * m.y.y + pair10 * m.z.y
|
||||||
|
result.y.x = pair1 * m.y.x + pair2 * m.z.x + pair5 * m.w.x
|
||||||
|
result.y.x -= pair0 * m.y.x + pair3 * m.z.x + pair4 * m.w.x
|
||||||
|
result.y.y = pair0 * m.x.x + pair7 * m.z.x + pair8 * m.w.x
|
||||||
|
result.y.y -= pair1 * m.x.x + pair6 * m.z.x + pair9 * m.w.x
|
||||||
|
result.y.z = pair3 * m.x.x + pair6 * m.y.x + pair11 * m.w.x
|
||||||
|
result.y.z -= pair2 * m.x.x + pair7 * m.y.x + pair10 * m.w.x
|
||||||
|
result.y.w = pair4 * m.x.x + pair9 * m.y.x + pair10 * m.z.x
|
||||||
|
result.y.w -= pair5 * m.x.x + pair8 * m.y.x + pair11 * m.z.x
|
||||||
|
|
||||||
|
pair0 = m.z.x * m.w.y
|
||||||
|
pair1 = m.w.x * m.z.y
|
||||||
|
pair2 = m.y.x * m.w.y
|
||||||
|
pair3 = m.w.x * m.y.y
|
||||||
|
pair4 = m.y.x * m.z.y
|
||||||
|
pair5 = m.z.x * m.y.y
|
||||||
|
pair6 = m.x.x * m.w.y
|
||||||
|
pair7 = m.w.x * m.x.y
|
||||||
|
pair8 = m.x.x * m.z.y
|
||||||
|
pair9 = m.z.x * m.x.y
|
||||||
|
pair10 = m.x.x * m.y.y
|
||||||
|
pair11 = m.y.x * m.x.y
|
||||||
|
|
||||||
|
result.z.x = pair0 * m.y.w + pair3 * m.z.w + pair4 * m.w.w
|
||||||
|
result.z.x -= pair1 * m.y.w + pair2 * m.z.w + pair5 * m.w.w
|
||||||
|
result.z.y = pair1 * m.x.w + pair6 * m.z.w + pair9 * m.w.w
|
||||||
|
result.z.y -= pair0 * m.x.w + pair7 * m.z.w + pair8 * m.w.w
|
||||||
|
result.z.z = pair2 * m.x.w + pair7 * m.y.w + pair10 * m.w.w
|
||||||
|
result.z.z -= pair3 * m.x.w + pair6 * m.y.w + pair11 * m.w.w
|
||||||
|
result.z.w = pair5 * m.x.w + pair8 * m.y.w + pair11 * m.z.w
|
||||||
|
result.z.w -= pair4 * m.x.w + pair9 * m.y.w + pair10 * m.z.w
|
||||||
|
result.w.x = pair2 * m.z.z + pair5 * m.w.z + pair1 * m.y.z
|
||||||
|
result.w.x -= pair4 * m.w.z + pair0 * m.y.z + pair3 * m.z.z
|
||||||
|
result.w.y = pair8 * m.w.z + pair0 * m.x.z + pair7 * m.z.z
|
||||||
|
result.w.y -= pair6 * m.z.z + pair9 * m.w.z + pair1 * m.x.z
|
||||||
|
result.w.z = pair6 * m.y.z + pair11 * m.w.z + pair3 * m.x.z
|
||||||
|
result.w.z -= pair10 * m.w.z + pair2 * m.x.z + pair7 * m.y.z
|
||||||
|
result.w.w = pair10 * m.z.z + pair4 * m.x.z + pair9 * m.y.z
|
||||||
|
result.w.w -= pair8 * m.y.z + pair11 * m.z.z + pair5 * m.x.z
|
||||||
|
|
||||||
|
val determinant = m.x.x * result.x.x + m.y.x * result.x.y + m.z.x * result.x.z + m.w.x * result.x.w
|
||||||
|
|
||||||
|
return result / determinant
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scale(s: Float3) = Mat4(Float4(x = s.x), Float4(y = s.y), Float4(z = s.z))
|
||||||
|
fun scale(m: Mat4) = scale(m.scale)
|
||||||
|
|
||||||
|
fun translation(t: Float3) = Mat4(w = Float4(t, 1.0f))
|
||||||
|
fun translation(m: Mat4) = translation(m.translation)
|
||||||
|
|
||||||
|
fun rotation(m: Mat4) = Mat4(normalize(m.right), normalize(m.up), normalize(m.forward))
|
||||||
|
fun rotation(d: Float3): Mat4 {
|
||||||
|
val r = transform(d, ::radians)
|
||||||
|
val c = transform(r, { x -> cos(x) })
|
||||||
|
val s = transform(r, { x -> sin(x) })
|
||||||
|
|
||||||
|
return Mat4.of(
|
||||||
|
c.y * c.z, -c.x * s.z + s.x * s.y * c.z, s.x * s.z + c.x * s.y * c.z, 0.0f,
|
||||||
|
c.y * s.z, c.x * c.z + s.x * s.y * s.z, -s.x * c.z + c.x * s.y * s.z, 0.0f,
|
||||||
|
-s.y , s.x * c.y , c.x * c.y , 0.0f,
|
||||||
|
0.0f , 0.0f , 0.0f , 1.0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fun rotation(axis: Float3, angle: Float): Mat4 {
|
||||||
|
val x = axis.x
|
||||||
|
val y = axis.y
|
||||||
|
val z = axis.z
|
||||||
|
|
||||||
|
val r = radians(angle)
|
||||||
|
val c = cos(r)
|
||||||
|
val s = sin(r)
|
||||||
|
val d = 1.0f - c
|
||||||
|
|
||||||
|
return Mat4.of(
|
||||||
|
x * x * d + c , x * y * d - z * s, x * z * d + y * s, 0.0f,
|
||||||
|
y * x * d + z * s, y * y * d + c , y * z * d - x * s, 0.0f,
|
||||||
|
z * x * d - y * s, z * y * d + x * s, z * z * d + c , 0.0f,
|
||||||
|
0.0f , 0.0f , 0.0f , 1.0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun normal(m: Mat4) = scale(1.0f / Float3(length2(m.right), length2(m.up), length2(m.forward))) * m
|
||||||
|
|
||||||
|
fun lookAt(eye: Float3, target: Float3, up: Float3 = Float3(z = 1.0f)): Mat4 {
|
||||||
|
return lookTowards(eye, target - eye, up)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lookTowards(eye: Float3, forward: Float3, up: Float3 = Float3(z = 1.0f)): Mat4 {
|
||||||
|
val f = normalize(forward)
|
||||||
|
val r = normalize(f x up)
|
||||||
|
val u = normalize(r x f)
|
||||||
|
return Mat4(Float4(r), Float4(u), Float4(f), Float4(eye, 1.0f))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun perspective(fov: Float, ratio: Float, near: Float, far: Float): Mat4 {
|
||||||
|
val t = 1.0f / tan(radians(fov) * 0.5f)
|
||||||
|
val a = (far + near) / (far - near)
|
||||||
|
val b = (2.0f * far * near) / (far - near)
|
||||||
|
val c = t / ratio
|
||||||
|
return Mat4(Float4(x = c), Float4(y = t), Float4(z = a, w = 1.0f), Float4(z = -b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ortho(l: Float, r: Float, b: Float, t: Float, n: Float, f: Float) = Mat4(
|
||||||
|
Float4(x = 2.0f / (r - 1.0f)),
|
||||||
|
Float4(y = 2.0f / (t - b)),
|
||||||
|
Float4(z = -2.0f / (f - n)),
|
||||||
|
Float4(-(r + l) / (r - l), -(t + b) / (t - b), -(f + n) / (f - n), 1.0f)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Romain Guy
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.curiouscreature.kotlin.math
|
||||||
|
|
||||||
|
data class Ray(var origin: Float3 = Float3(), var direction: Float3)
|
||||||
|
|
||||||
|
fun pointAt(r: Ray, t: Float) = r.origin + r.direction * t
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Romain Guy
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("NOTHING_TO_INLINE")
|
||||||
|
|
||||||
|
package com.curiouscreature.kotlin.math
|
||||||
|
|
||||||
|
const val PI = 3.1415926536f
|
||||||
|
const val HALF_PI = PI * 0.5f
|
||||||
|
const val TWO_PI = PI * 2.0f
|
||||||
|
const val FOUR_PI = PI * 4.0f
|
||||||
|
const val INV_PI = 1.0f / PI
|
||||||
|
const val INV_TWO_PI = INV_PI * 0.5f
|
||||||
|
const val INV_FOUR_PI = INV_PI * 0.25f
|
||||||
|
|
||||||
|
inline fun clamp(x: Float, min: Float, max: Float)= if (x < min) min else (if (x > max) max else x)
|
||||||
|
|
||||||
|
inline fun saturate(x: Float) = clamp(x, 0.0f, 1.0f)
|
||||||
|
|
||||||
|
inline fun mix(a: Float, b: Float, x: Float) = a * (1.0f - x) + b * x
|
||||||
|
|
||||||
|
inline fun degrees(v: Float) = v * (180.0f * INV_PI)
|
||||||
|
|
||||||
|
inline fun radians(v: Float) = v * (PI / 180.0f)
|
||||||
|
|
||||||
|
inline fun fract(v: Float) = v % 1
|
||||||
|
|
||||||
|
inline fun sqr(v: Float) = v * v
|
||||||
|
|
||||||
|
//inline fun pow(x: Float, y: Float) = StrictMath.pow(x.toDouble(), y.toDouble()).toFloat()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
|||||||
package com.helible.pilot
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothSocket
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class TransferFailedException : IOException("Reading incoming data failed")
|
|
||||||
|
|
||||||
class BluetoothDataTransferService(
|
|
||||||
private val socket: BluetoothSocket,
|
|
||||||
) {
|
|
||||||
fun listenForIncomingMessages(): Flow<KMessage> {
|
|
||||||
return flow {
|
|
||||||
if (!socket.isConnected)
|
|
||||||
return@flow
|
|
||||||
val buffer = ByteArray(128)
|
|
||||||
while (true) {
|
|
||||||
val byteCount: Int = try {
|
|
||||||
socket.inputStream.read(buffer)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw TransferFailedException()
|
|
||||||
}
|
|
||||||
emit(
|
|
||||||
buffer.decodeToString(
|
|
||||||
endIndex = byteCount
|
|
||||||
).toKMessage()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.flowOn(Dispatchers.IO)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMessage(bytes: ByteArray): Boolean {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
socket.outputStream.write(bytes)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return@withContext false
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.helible.pilot
|
|
||||||
|
|
||||||
// Todo: add checksum
|
|
||||||
// Todo: add arguments names
|
|
||||||
|
|
||||||
data class KMessage(
|
|
||||||
val r1: UShort,
|
|
||||||
val r2: UShort,
|
|
||||||
val r3: UShort,
|
|
||||||
val emergStop: Boolean,
|
|
||||||
val alarm: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun KMessage.toByteArray(): ByteArray {
|
|
||||||
return "$$r1;$r2;$r3;$emergStop;$alarm\r\n".encodeToByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.toKMessage(): KMessage {
|
|
||||||
// TODO: implement
|
|
||||||
return KMessage(
|
|
||||||
0u, 0u, 0u, emergStop = false, alarm = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -15,27 +15,29 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.helible.pilot.components.scannerScreen.BluetoothScannerScreen
|
import com.helible.pilot.components.CalibrationPage
|
||||||
|
import com.helible.pilot.components.NotImplementedPage
|
||||||
|
import com.helible.pilot.components.RotorsTestPage
|
||||||
|
import com.helible.pilot.components.console.ConsolePage
|
||||||
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
||||||
import com.helible.pilot.viewmodels.AppPreferences
|
|
||||||
import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
|
||||||
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
||||||
|
import com.helible.pilot.components.PidSettingsPage
|
||||||
|
import com.helible.pilot.components.scannerScreen.ScannerScreen
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
import com.helible.pilot.permissions.PermissionsLauncher
|
import com.helible.pilot.permissions.PermissionsLauncher
|
||||||
import com.helible.pilot.permissions.PermissionsRequest
|
import com.helible.pilot.permissions.PermissionsRequest
|
||||||
import com.helible.pilot.permissions.RequestHardwareFeatures
|
import com.helible.pilot.permissions.RequestHardwareFeatures
|
||||||
import com.helible.pilot.ui.theme.TestblueTheme
|
import com.helible.pilot.ui.theme.TestblueTheme
|
||||||
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
import com.helible.pilot.viewmodels.BluetoothViewModel
|
import com.helible.pilot.viewmodels.BluetoothViewModel
|
||||||
import com.helible.pilot.viewmodels.BluetoothViewModelFactory
|
import com.helible.pilot.viewmodels.BluetoothViewModelFactory
|
||||||
import com.helible.pilot.viewmodels.PermissionDialogViewModel
|
import com.helible.pilot.viewmodels.PermissionDialogViewModel
|
||||||
import com.helible.pilot.viewmodels.PreferencesViewModel
|
import com.helible.pilot.viewmodels.PreferencesViewModel
|
||||||
|
import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// TODO: device screen logic
|
|
||||||
// TODO: constrain text size
|
|
||||||
// TODO: add Bluetooth telemetry...
|
|
||||||
// TODO: move text strings to resources
|
// TODO: move text strings to resources
|
||||||
// TODO: review permissions logic
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
||||||
@@ -44,6 +46,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
PreferencesViewModel(preferences)
|
PreferencesViewModel(preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
@@ -61,19 +64,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val bluetoothState by bluetoothViewModel.state.collectAsState()
|
val bluetoothState by bluetoothViewModel.state.collectAsState()
|
||||||
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
|
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
|
||||||
|
val rotorsDuty by bluetoothViewModel.rotorsDuty.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(key1 = null) {
|
LaunchedEffect(key1 = null) {
|
||||||
permissionLauncher.launch()
|
permissionLauncher.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = bluetoothState) {
|
|
||||||
if (bluetoothState.isConnected) {
|
|
||||||
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
||||||
@@ -101,7 +97,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
startDestination = "device"
|
startDestination = "device"
|
||||||
) {
|
) {
|
||||||
composable("scanner") {
|
composable("scanner") {
|
||||||
BluetoothScannerScreen(
|
ScannerScreen(
|
||||||
bluetoothState = bluetoothState,
|
bluetoothState = bluetoothState,
|
||||||
selectedDevice = selectedDevice,
|
selectedDevice = selectedDevice,
|
||||||
startScan = { bluetoothViewModel.startScan() },
|
startScan = { bluetoothViewModel.startScan() },
|
||||||
@@ -139,34 +135,107 @@ class MainActivity : ComponentActivity() {
|
|||||||
device
|
device
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
disconnectFromDevice = { bluetoothViewModel.disconnectFromDevice() },
|
disconnectFromDevice = {
|
||||||
|
preferencesViewModel.clearPreferences()
|
||||||
|
bluetoothViewModel.disconnectFromDevice()
|
||||||
|
},
|
||||||
deviceActionsList = defaultDeviceActionsList()
|
deviceActionsList = defaultDeviceActionsList()
|
||||||
)
|
)
|
||||||
if (preferencesViewModel.preferences != null) BackHandler {}
|
if (preferencesViewModel.preferences != null) BackHandler {}
|
||||||
}
|
}
|
||||||
composable("console")
|
composable("console/{title}")
|
||||||
{
|
{ _ ->
|
||||||
|
ConsolePage(
|
||||||
|
startTakeoff = { bluetoothViewModel.startTakeoff() },
|
||||||
|
startOnboarding = { bluetoothViewModel.startOnboarding() },
|
||||||
|
stop = { bluetoothViewModel.stopRotors() },
|
||||||
|
changeStick1Position = {
|
||||||
|
_: Int, heightVelocity: Int ->
|
||||||
|
bluetoothViewModel.changeHeightStickPosition(heightVelocity)
|
||||||
|
},
|
||||||
|
changeStick2Position = {
|
||||||
|
x: Int, y: Int ->
|
||||||
|
bluetoothViewModel.changePitchStickPosition(y)
|
||||||
|
bluetoothViewModel.changeYawStickPosition(x)
|
||||||
|
},
|
||||||
|
bluetoothUiState = bluetoothState,
|
||||||
|
reconnect = {
|
||||||
|
val preferences = preferences.getPreferences()
|
||||||
|
if(preferences != null) {
|
||||||
|
bluetoothViewModel.connectToDevice(
|
||||||
|
preferences.deviceAddress
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
navController.navigate("scanner")
|
||||||
}
|
}
|
||||||
composable("codeblocks")
|
},
|
||||||
{
|
navigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
composable("imu_calibration")
|
)
|
||||||
{
|
BackHandler {
|
||||||
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
composable("motor_test")
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
composable("pid_settings")
|
composable("codeblocks/{title}")
|
||||||
{
|
{ backStackEntry ->
|
||||||
|
NotImplementedPage(
|
||||||
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
|
navigateBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable("reports")
|
composable("imu_calibration/{title}")
|
||||||
{
|
{ backStackEntry ->
|
||||||
|
CalibrationPage(
|
||||||
|
deviceStatus = bluetoothState.deviceState?.status,
|
||||||
|
startCalibration = { bluetoothViewModel.startImuCalibration() },
|
||||||
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
|
navigateBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("motor_test/{title}")
|
||||||
|
{ backStackEntry ->
|
||||||
|
RotorsTestPage(
|
||||||
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
|
rotorsDuty = rotorsDuty,
|
||||||
|
setRotorsDuty = { bluetoothViewModel.setRotorsDuty(it) },
|
||||||
|
startTelemetrySending = { bluetoothViewModel.startRotorsConfigurationTelemetry() },
|
||||||
|
stopRotors = { bluetoothViewModel.stopRotors() },
|
||||||
|
navigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
bluetoothViewModel.stopRotorsConfigurationTelemetry()
|
||||||
|
bluetoothViewModel.stopRotors()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
BackHandler {
|
||||||
|
navController.popBackStack()
|
||||||
|
bluetoothViewModel.stopRotorsConfigurationTelemetry()
|
||||||
|
bluetoothViewModel.stopRotors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("pid_settings/{title}")
|
||||||
|
{ backStackEntry ->
|
||||||
|
PidSettingsPage(
|
||||||
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
|
navigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
bluetoothViewModel.clearPidSettings()
|
||||||
|
},
|
||||||
|
requestPidSettings = { bluetoothViewModel.requestPidSettings() },
|
||||||
|
setPidSettings = {settings -> bluetoothViewModel.applyPidSettings(settings)},
|
||||||
|
deviceState = bluetoothState.deviceState
|
||||||
|
)
|
||||||
|
BackHandler {
|
||||||
|
navController.popBackStack()
|
||||||
|
bluetoothViewModel.clearPidSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("reports/{title}")
|
||||||
|
{ backStackEntry ->
|
||||||
|
NotImplementedPage(
|
||||||
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
|
navigateBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
app/src/main/java/com/helible/pilot/components/BlankPage.kt
Normal file
59
app/src/main/java/com/helible/pilot/components/BlankPage.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BlankPage(title: String, navigateBack: () -> Unit, block: @Composable () -> Unit) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { navigateBack() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun BlankPagePreview() {
|
||||||
|
Surface {
|
||||||
|
BlankPage(title = "Blank page", navigateBack = {}) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(5.dp)) {
|
||||||
|
Text(text = "This page in development")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.widget.Spinner
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.ChangedDeviceStatus
|
||||||
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CalibrationPage(
|
||||||
|
deviceStatus: DeviceStatus?,
|
||||||
|
title: String,
|
||||||
|
startCalibration: () -> Unit,
|
||||||
|
navigateBack: () -> Unit
|
||||||
|
) {
|
||||||
|
BlankPage(title = title, navigateBack = navigateBack) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.calibration_description),
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
enabled = deviceStatus != DeviceStatus.IsImuCalibration,
|
||||||
|
onClick = startCalibration,
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
) {
|
||||||
|
if(deviceStatus != DeviceStatus.IsImuCalibration) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.tune),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(3.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Начать калибровку"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CircularProgressIndicator ()
|
||||||
|
Text(
|
||||||
|
text = "Калибровка...",
|
||||||
|
modifier = Modifier.padding(5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun CalibrationPagePreview() {
|
||||||
|
Surface {
|
||||||
|
CalibrationPage(
|
||||||
|
DeviceStatus.IsImuCalibration,
|
||||||
|
title = "Калибровка гироскопа и акселерометра",
|
||||||
|
startCalibration = {},
|
||||||
|
navigateBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import androidx.compose.material3.Divider
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RequiredHardwareFeatures(
|
fun RequiredHardwareFeatures(
|
||||||
@@ -36,3 +36,16 @@ fun RequiredHardwareFeatures(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RequiredHardwareFeaturesPreview() {
|
||||||
|
RequiredHardwareFeatures(
|
||||||
|
title = "Turn on Bluetooth",
|
||||||
|
description = "App requires Bluetooth turned on to continue",
|
||||||
|
confirmButtonText = "Turn on",
|
||||||
|
featureState = false,
|
||||||
|
requestFeature = {},
|
||||||
|
onDismissRequest = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.components.BlankPage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotImplementedPage(title: String, navigateBack: () -> Unit) {
|
||||||
|
BlankPage(title = title, navigateBack = navigateBack) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)) {
|
||||||
|
Text(text = "Эта страница пока не готова и находится на стадии разработки")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,14 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -82,6 +83,16 @@ fun PermissionDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PermissionDialogPreview() {
|
||||||
|
PermissionDialog(
|
||||||
|
LocationPermissionTextProvider(),
|
||||||
|
false,
|
||||||
|
{}, {}, {}, {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface PermissionTextProvider {
|
interface PermissionTextProvider {
|
||||||
fun getDescription(isPermanentDeclined: Boolean): String
|
fun getDescription(isPermanentDeclined: Boolean): String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
|
import com.helible.pilot.dataclasses.PidParams
|
||||||
|
import com.helible.pilot.dataclasses.PidSettings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PidSettingsPage(
|
||||||
|
title: String,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
requestPidSettings: () -> Unit,
|
||||||
|
setPidSettings: (PidSettings) -> Unit,
|
||||||
|
deviceState: DeviceState?,
|
||||||
|
) {
|
||||||
|
BlankPage(title = title, navigateBack = navigateBack) {
|
||||||
|
var pValue by remember { mutableStateOf("") }
|
||||||
|
var iValue by remember { mutableStateOf("") }
|
||||||
|
var dValue by remember { mutableStateOf("") }
|
||||||
|
val dropdownMenuItems =
|
||||||
|
listOf("Контроллер высоты", "Контроллер тангажа", "Контроллер рысканья")
|
||||||
|
var selectedRegulator by remember { mutableStateOf(dropdownMenuItems[0]) }
|
||||||
|
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
requestPidSettings()
|
||||||
|
}
|
||||||
|
LaunchedEffect(deviceState?.pidSettings) {
|
||||||
|
if (deviceState?.pidSettings != null) {
|
||||||
|
val pidSettings = deviceState.pidSettings
|
||||||
|
when (selectedRegulator) {
|
||||||
|
dropdownMenuItems[0] -> {
|
||||||
|
pidSettings.heightControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.heightControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.heightControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownMenuItems[1] -> {
|
||||||
|
pidSettings.yawControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.yawControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.yawControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownMenuItems[2] -> {
|
||||||
|
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (deviceState?.status != DeviceStatus.Idle) {
|
||||||
|
Text(
|
||||||
|
text = "Этот раздел доступен только с подключенным заряженным бездействующим устройством.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else if (deviceState.pidSettings == null) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(10.dp))
|
||||||
|
Text(text = "Синхронизация...")
|
||||||
|
} else {
|
||||||
|
val pidSettings = deviceState.pidSettings
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 10.dp).padding(bottom = 10.dp)) {
|
||||||
|
Text(
|
||||||
|
"Рекомендации по настройке ПИД регуляторов",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.p_pid_value_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.i_pid_value_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.d_pid_value_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedDropdownMenu(
|
||||||
|
label = "ПИД регулятор",
|
||||||
|
suggestions = dropdownMenuItems,
|
||||||
|
onChange = { selected ->
|
||||||
|
Log.i("BluetoothVM", selected)
|
||||||
|
when (dropdownMenuItems.indexOf(selected)) {
|
||||||
|
0 -> {
|
||||||
|
selectedRegulator = dropdownMenuItems[0]
|
||||||
|
pidSettings.heightControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.heightControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.heightControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
selectedRegulator = dropdownMenuItems[1]
|
||||||
|
pidSettings.yawControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.yawControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.yawControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
selectedRegulator = dropdownMenuItems[2]
|
||||||
|
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
|
||||||
|
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
|
||||||
|
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = pValue,
|
||||||
|
onValueChange = { pValue = it },
|
||||||
|
label = { Text(text = "P") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = iValue,
|
||||||
|
onValueChange = { iValue = it },
|
||||||
|
label = { Text(text = "I") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dValue,
|
||||||
|
onValueChange = { dValue = it },
|
||||||
|
label = { Text(text = "D") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
val p = pValue
|
||||||
|
val i = iValue
|
||||||
|
val d = dValue
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
when (dropdownMenuItems.indexOf(selectedRegulator)) {
|
||||||
|
0 -> {
|
||||||
|
val newPidSettings = pidSettings.copy(
|
||||||
|
heightControllerParams = PidParams(
|
||||||
|
p.toFloat(),
|
||||||
|
i.toFloat(),
|
||||||
|
d.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setPidSettings(newPidSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
val newPidSettings = pidSettings.copy(
|
||||||
|
yawControllerParams = PidParams(
|
||||||
|
p.toFloat(),
|
||||||
|
i.toFloat(),
|
||||||
|
d.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setPidSettings(newPidSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
val newPidSettings = pidSettings.copy(
|
||||||
|
pitchControllerParams = PidParams(
|
||||||
|
p.toFloat(),
|
||||||
|
i.toFloat(),
|
||||||
|
d.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setPidSettings(newPidSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isValidValue(p) && isValidValue(i) && isValidValue(d)
|
||||||
|
) {
|
||||||
|
Text(text = "Применить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidValue(k: String): Boolean {
|
||||||
|
return k.toFloatOrNull() != null && k.toFloat() >= 0f && k.toFloat() <= 15f
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun PidSettingsPreview() {
|
||||||
|
PidSettingsPage(
|
||||||
|
title = "Настройки ПИД регуляторов",
|
||||||
|
navigateBack = { },
|
||||||
|
requestPidSettings = { },
|
||||||
|
setPidSettings = {},
|
||||||
|
deviceState = DeviceState(
|
||||||
|
status = DeviceStatus.Idle,
|
||||||
|
pidSettings = PidSettings(
|
||||||
|
PidParams(1f, 1f, 1f),
|
||||||
|
PidParams(1f, 1f, 1f),
|
||||||
|
PidParams(1f, 1f, 1f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun DropdownDemo() {
|
||||||
|
OutlinedDropdownMenu(label = "", suggestions = listOf("A", "B"), onChange = {})
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun OutlinedDropdownMenu(
|
||||||
|
label: String,
|
||||||
|
suggestions: List<String>,
|
||||||
|
onChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var selectedText by remember { mutableStateOf(suggestions.first()) }
|
||||||
|
var textfieldSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
|
||||||
|
val icon = if (expanded)
|
||||||
|
Icons.Filled.KeyboardArrowUp
|
||||||
|
else
|
||||||
|
Icons.Filled.KeyboardArrowDown
|
||||||
|
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedText,
|
||||||
|
onValueChange = {
|
||||||
|
selectedText = it
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
// This value is used to assign to the DropDown the same width
|
||||||
|
textfieldSize = coordinates.size.toSize()
|
||||||
|
},
|
||||||
|
label = { Text(label) },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(icon, "contentDescription",
|
||||||
|
Modifier.clickable { expanded = !expanded })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
modifier = Modifier
|
||||||
|
.width(with(LocalDensity.current) { textfieldSize.width.toDp() })
|
||||||
|
) {
|
||||||
|
suggestions.forEach { label ->
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
selectedText = label
|
||||||
|
expanded = false
|
||||||
|
onChange(selectedText)
|
||||||
|
},
|
||||||
|
text = { Text(label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
app/src/main/java/com/helible/pilot/components/RotorsTestPage.kt
Normal file
119
app/src/main/java/com/helible/pilot/components/RotorsTestPage.kt
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package com.helible.pilot.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.dataclasses.RotorsDuty
|
||||||
|
import com.helible.pilot.ui.theme.TestblueTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RotorsTestPage(
|
||||||
|
title: String,
|
||||||
|
rotorsDuty: RotorsDuty,
|
||||||
|
setRotorsDuty: (duty: RotorsDuty) -> Unit,
|
||||||
|
startTelemetrySending: () -> Unit,
|
||||||
|
stopRotors: () -> Unit,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
BlankPage(title = title, navigateBack = navigateBack) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
startTelemetrySending()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "R1",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(top = 10.dp)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = rotorsDuty.r1.toFloat(),
|
||||||
|
onValueChange = { setRotorsDuty(rotorsDuty.copy(r1 = it.toInt().toShort())) },
|
||||||
|
valueRange = 0f..5000f,
|
||||||
|
steps = 10
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "При перемещении слайдера вправо ротор 1 должен вращаться против часовой стрелки, если смотреть сверху.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "R2",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(top = 10.dp)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = rotorsDuty.r2.toFloat(),
|
||||||
|
onValueChange = { setRotorsDuty(rotorsDuty.copy(r2 = it.toInt().toShort())) },
|
||||||
|
valueRange = 0f..5000f,
|
||||||
|
steps = 10
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "При отклонении слайдера вправо от центра ротор 1 должен вращаться по часовой стрелке, а при отклонении влево - против часовой, если смотреть сверху.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "R3",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(top = 10.dp)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = rotorsDuty.r3.toFloat(),
|
||||||
|
onValueChange = { setRotorsDuty(rotorsDuty.copy(r3 = it.toInt().toShort())) },
|
||||||
|
valueRange = -5000f..5000f,
|
||||||
|
steps = 10
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "При перемещении слайдера вправо ротор 1 должен вращаться по часовой стрелке, если смотреть сверху.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { stopRotors() },
|
||||||
|
containerColor = Color(245, 47, 7),
|
||||||
|
modifier = Modifier.padding(top = 10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "СТОП",
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun RotorsTestPagePreview() {
|
||||||
|
TestblueTheme {
|
||||||
|
RotorsTestPage(
|
||||||
|
title = "Тестирование моторов",
|
||||||
|
rotorsDuty = RotorsDuty(5, 5, 5),
|
||||||
|
setRotorsDuty = { _ -> },
|
||||||
|
startTelemetrySending = {},
|
||||||
|
stopRotors = {},
|
||||||
|
navigateBack = { /*TODO*/ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
app/src/main/java/com/helible/pilot/components/World.kt
Normal file
63
app/src/main/java/com/helible/pilot/components/World.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.curiouscreature.kotlin.math.Float3
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun World(mesh: Mesh = cube, drawVertices: MutableState<Boolean>, camerax: MutableState<Float>) {
|
||||||
|
val camera = Camera(
|
||||||
|
position = Float3(20f, 110.1f, 110.0f),
|
||||||
|
target = Float3(0.1f, camerax.value, 1.0f)
|
||||||
|
)
|
||||||
|
Text("Camera: ${camera.target.y}")
|
||||||
|
val animatedProgress by rememberInfiniteTransition().animateFloat(
|
||||||
|
initialValue = 0.01f,
|
||||||
|
targetValue = 360f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 10000, easing = LinearEasing),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Canvas(Modifier.size(50.dp, 50.dp)) {
|
||||||
|
mesh.rotation = Float3(animatedProgress + 90, animatedProgress + 180, 0.01f)
|
||||||
|
camera.target.y += 0.1f
|
||||||
|
camera.target.x += 0.1f
|
||||||
|
camera.target.z -= 0.1f
|
||||||
|
val lines = render3d(camera, mesh)
|
||||||
|
lines.forEach { (one, two, three) ->
|
||||||
|
if (drawVertices.value) {
|
||||||
|
drawCircle(color = Color.Cyan, radius = 5f, center = Offset(one.x, one.y))
|
||||||
|
drawCircle(color = Color.Cyan, radius = 5f, center = Offset(two.x, two.y))
|
||||||
|
drawCircle(color = Color.Cyan, radius = 5f, center = Offset(three.x, three.y))
|
||||||
|
}
|
||||||
|
drawLine(
|
||||||
|
color = Color.Red,
|
||||||
|
start = Offset(one.x, one.y),
|
||||||
|
end = Offset(two.x, two.y)
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = Color.Red,
|
||||||
|
start = Offset(two.x, two.y),
|
||||||
|
end = Offset(three.x, three.y)
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = Color.Red,
|
||||||
|
start = Offset(three.x, three.y),
|
||||||
|
end = Offset(one.x, one.y)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package com.helible.pilot.components.console
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ExitToApp
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
|
import com.manalkaff.jetstick.JoyStick
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConsolePage(
|
||||||
|
startTakeoff: () -> Unit,
|
||||||
|
startOnboarding: () -> Unit,
|
||||||
|
stop: () -> Unit,
|
||||||
|
reconnect: () -> Unit,
|
||||||
|
changeStick1Position: (x: Int, y: Int) -> Unit,
|
||||||
|
changeStick2Position: (x: Int, y: Int) -> Unit,
|
||||||
|
bluetoothUiState: BluetoothUiState,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
val deviceState = bluetoothUiState.deviceState
|
||||||
|
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||||
|
LaunchedEffect(key1 = null) {
|
||||||
|
Log.i("Console", "state: ${bluetoothUiState.deviceState}, isConnected: ${bluetoothUiState.isConnected}")
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
if (bluetoothUiState.isConnected && deviceState != null) {
|
||||||
|
Text("Высота полёта: ${deviceState.flightHeight / 100} м; ")
|
||||||
|
Text("Рыскание: ${deviceState.yaw}°; ")
|
||||||
|
Text("Тангаж: ${deviceState.pitch}°; ")
|
||||||
|
Text("Крен: ${deviceState.roll}°; ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
|
||||||
|
JoyStick(
|
||||||
|
size = 200.dp,
|
||||||
|
dotSize = 80.dp,
|
||||||
|
backgroundImage = R.drawable.stick_background,
|
||||||
|
dotImage = R.drawable.stick_dot,
|
||||||
|
modifier = Modifier.padding(30.dp)
|
||||||
|
) { x: Float, y: Float ->
|
||||||
|
changeStick1Position(x.toInt(), y.toInt())
|
||||||
|
Log.d("JoyStick", "$x, $y")
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
if (bluetoothUiState.isConnected && deviceState != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (deviceState.status == DeviceStatus.IsPreparingForTakeoff) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(7.dp))
|
||||||
|
}
|
||||||
|
Text(text = describeStatus(deviceState.status))
|
||||||
|
}
|
||||||
|
Text(text = "Заряд батареи: ${deviceState.batteryCharge}%")
|
||||||
|
} else {
|
||||||
|
Text(text = "Нет соединения с устройством")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bluetoothUiState.isConnecting) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(5.dp))
|
||||||
|
Text(text = "Подключение...")
|
||||||
|
}
|
||||||
|
} else if (bluetoothUiState.isConnected) {
|
||||||
|
Button(
|
||||||
|
onClick = startTakeoff,
|
||||||
|
enabled = deviceState?.status == DeviceStatus.Idle
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowUp, contentDescription = null)
|
||||||
|
Text(text = "Взлёт")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(onClick = stop, colors = ButtonDefaults.buttonColors(Color.Red)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.Warning, contentDescription = null)
|
||||||
|
Text(text = "СТОП")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = startOnboarding,
|
||||||
|
enabled = deviceState?.status in listOf(
|
||||||
|
DeviceStatus.IsFlying, DeviceStatus.IsPreparingForTakeoff
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowDown, contentDescription = null)
|
||||||
|
Text(text = "Посадка")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(onClick = reconnect) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Refresh, contentDescription = null)
|
||||||
|
Text(text = "Попробовать ещё раз")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!bluetoothUiState.isEnabled || deviceState?.status in listOf(DeviceStatus.ChargeRequired, DeviceStatus.Idle, null)) {
|
||||||
|
Button(
|
||||||
|
onClick = navigateBack
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.ExitToApp, contentDescription = null)
|
||||||
|
Text(text = "Выход")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JoyStick(
|
||||||
|
size = 200.dp,
|
||||||
|
dotSize = 80.dp,
|
||||||
|
backgroundImage = R.drawable.stick_background,
|
||||||
|
dotImage = R.drawable.stick_dot,
|
||||||
|
) { x: Float, y: Float ->
|
||||||
|
changeStick2Position(x.toInt(), y.toInt())
|
||||||
|
Log.d("JoyStick", "$x, $y")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LockScreenOrientation(orientation: Int) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DisposableEffect(orientation) {
|
||||||
|
val activity = context.findActivity() ?: return@DisposableEffect onDispose {}
|
||||||
|
val originalOrientation = activity.requestedOrientation
|
||||||
|
activity.requestedOrientation = orientation
|
||||||
|
onDispose {
|
||||||
|
// restore original orientation when view disappears
|
||||||
|
activity.requestedOrientation = originalOrientation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.findActivity(): Activity? = when (this) {
|
||||||
|
is Activity -> this
|
||||||
|
is ContextWrapper -> baseContext.findActivity()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun describeStatus(deviceState: DeviceStatus): String {
|
||||||
|
return when (deviceState) {
|
||||||
|
DeviceStatus.Idle -> {
|
||||||
|
"Устройство заряжено, бездействует"
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceStatus.IsFlying -> {
|
||||||
|
"В полёте"
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceStatus.ChargeRequired -> {
|
||||||
|
"Устройство разряжено. Дальнейшие полёты невозможны"
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceStatus.IsImuCalibration -> {
|
||||||
|
"Калибровка гироскопа и акселерометра. Пожалуйста, не двигайте устройство."
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceStatus.IsPreparingForTakeoff -> {
|
||||||
|
"Подготовка ко взлёту..."
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceStatus.IsBoarding -> {
|
||||||
|
"Посадка"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, device = "spec:parent=pixel_5,orientation=landscape")
|
||||||
|
@Composable
|
||||||
|
fun ConsolePreview() {
|
||||||
|
ConsolePage(
|
||||||
|
startTakeoff = {},
|
||||||
|
startOnboarding = {},
|
||||||
|
stop = {},
|
||||||
|
changeStick1Position = { _: Int, _: Int -> },
|
||||||
|
changeStick2Position = { _: Int, _: Int -> },
|
||||||
|
bluetoothUiState = BluetoothUiState().copy(
|
||||||
|
isConnected = false,
|
||||||
|
deviceState = null
|
||||||
|
),
|
||||||
|
reconnect = {},
|
||||||
|
navigateBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -26,13 +27,15 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.R
|
import com.helible.pilot.R
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
import com.helible.pilot.viewmodels.AppPreferences
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceBadge(
|
fun DeviceBadge(
|
||||||
bluetoothUiState: BluetoothUiState,
|
bluetoothUiState: BluetoothUiState,
|
||||||
tryToReconnect: () -> Unit,
|
tryToReconnect: () -> Unit,
|
||||||
getPreferences: () -> AppPreferences?
|
getPreferences: () -> AppPreferences?,
|
||||||
) {
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -65,7 +68,19 @@ fun DeviceBadge(
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
DeviceConnectionStatus(bluetoothUiState)
|
DeviceConnectionStatus(bluetoothUiState)
|
||||||
Text(text = "Заряд батареи: 79%")
|
if(bluetoothUiState.isConnected) {
|
||||||
|
val deviceStatus = bluetoothUiState.deviceState?.status
|
||||||
|
if (deviceStatus != null) {
|
||||||
|
Text(text = "Заряд батареи: ${bluetoothUiState.deviceState.batteryCharge}%")
|
||||||
|
if (deviceStatus == DeviceStatus.ChargeRequired) {
|
||||||
|
Text(text = "Аккумулятор разряжен", color = Color.Red)
|
||||||
|
} else {
|
||||||
|
Text(text = deviceStatus.description())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(text = "Ожиданием рукопожатия...")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
@@ -89,7 +104,14 @@ fun DeviceBadge(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DeviceBadgePreview() {
|
fun DeviceBadgePreview() {
|
||||||
DeviceBadge(
|
DeviceBadge(
|
||||||
bluetoothUiState = BluetoothUiState(isConnected = true),
|
bluetoothUiState = BluetoothUiState(
|
||||||
|
isConnected = false,
|
||||||
|
isConnecting = true,
|
||||||
|
deviceState = DeviceState(
|
||||||
|
status = DeviceStatus.Idle,
|
||||||
|
batteryCharge = 50,
|
||||||
|
)
|
||||||
|
),
|
||||||
tryToReconnect = {},
|
tryToReconnect = {},
|
||||||
getPreferences = { AppPreferences("Helicopter", "AA:BB:CC:FF:DD") }
|
getPreferences = { AppPreferences("Helicopter", "AA:BB:CC:FF:DD") }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
|||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
)
|
)
|
||||||
Text("На связи")
|
Text("На связи")
|
||||||
}
|
} else if (bluetoothState.errorMessage != null) {
|
||||||
else if (bluetoothState.errorMessage != null) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.cancel),
|
painter = painterResource(id = R.drawable.cancel),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -42,8 +41,7 @@ fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
|||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
)
|
)
|
||||||
Text("Ошибка: ${bluetoothState.errorMessage}")
|
Text("Ошибка: ${bluetoothState.errorMessage}")
|
||||||
}
|
} else if (bluetoothState.isConnecting) {
|
||||||
else if (bluetoothState.isConnecting) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.sync),
|
painter = painterResource(id = R.drawable.sync),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -53,6 +51,8 @@ fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
|||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
)
|
)
|
||||||
Text("Подключение...")
|
Text("Подключение...")
|
||||||
|
} else {
|
||||||
|
Text("Попытка подключения не удалась.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
package com.helible.pilot.components.deviceScreen
|
package com.helible.pilot.components.deviceScreen
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -25,13 +17,12 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.R
|
import com.helible.pilot.R
|
||||||
import com.helible.pilot.components.Title
|
import com.helible.pilot.components.scannerScreen.Title
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
import com.helible.pilot.viewmodels.AppPreferences
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
|
||||||
@@ -46,7 +37,7 @@ fun DeviceControlScreen(
|
|||||||
scannerPageName: String = "scanner",
|
scannerPageName: String = "scanner",
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val preferences: AppPreferences? = getPreferences()
|
val preferences = getPreferences()
|
||||||
if (preferences == null) {
|
if (preferences == null) {
|
||||||
navigateToPage(scannerPageName)
|
navigateToPage(scannerPageName)
|
||||||
} else {
|
} else {
|
||||||
@@ -90,13 +81,21 @@ fun DeviceControlScreen(
|
|||||||
|
|
||||||
Column(modifier = Modifier.padding(horizontal = 3.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 3.dp)) {
|
||||||
for (section in deviceActionsList) {
|
for (section in deviceActionsList) {
|
||||||
Text(section.key,
|
Text(
|
||||||
|
section.key,
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
)
|
)
|
||||||
for (action in section.value) {
|
for (action in section.value) {
|
||||||
TextButton(onClick = { /* TODO */}) {
|
TextButton(
|
||||||
|
onClick = { navigateToPage(action.first + '/' + action.second.second) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = action.second.first.first),
|
painter = painterResource(id = action.second.first.first),
|
||||||
tint = action.second.first.second,
|
tint = action.second.first.second,
|
||||||
@@ -105,12 +104,12 @@ fun DeviceControlScreen(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = action.second.second,
|
text = action.second.second,
|
||||||
color = MaterialTheme.colorScheme.inverseSurface,
|
color = MaterialTheme.colorScheme.inverseSurface
|
||||||
modifier = Modifier.padding(horizontal = 5.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
disconnectFromDevice()
|
disconnectFromDevice()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -22,8 +21,9 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
|
||||||
import com.helible.pilot.R
|
import com.helible.pilot.R
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,7 +44,12 @@ fun DeviceItem(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(8.dp)) {
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f, true)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = deviceInfo.name,
|
text = deviceInfo.name,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -56,9 +61,10 @@ fun DeviceItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (deviceInfo.isScanned) {
|
if (deviceInfo.isScanned) {
|
||||||
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
|
val icon = getSignalIconForRssiValue(deviceInfo.rssi)
|
||||||
|
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.weight(0.3f)) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)),
|
painterResource(id = icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.components.scannerScreen.DeviceItem
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoveredDevicesList(
|
fun DiscoveredDevicesList(
|
||||||
@@ -102,3 +103,26 @@ fun DiscoveredDevicesList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DiscoveredDevicesListPreview() {
|
||||||
|
val state = BluetoothUiState(
|
||||||
|
pairedBluetoothDevices = listOf(
|
||||||
|
BluetoothDevice("My car", "AA:BB:CC:DD:FF", -70, false),
|
||||||
|
BluetoothDevice("Speaker", "AA:BB:CC:DD:FF", -20, false),
|
||||||
|
BluetoothDevice("My TV", "AA:BB:CC:DD:FF", 10, false),
|
||||||
|
BluetoothDevice("My phone", "AA:BB:CC:DD:FF", -50, false),
|
||||||
|
BluetoothDevice("Mi Band 6", "AA:BB:CC:DD:FF", -100, false),
|
||||||
|
),
|
||||||
|
scannedBluetoothDevices = listOf(
|
||||||
|
BluetoothDevice("Watch", "AA:BB:CC:DD:FF", -10, true),
|
||||||
|
BluetoothDevice("Mi Cleaner", "AA:BB:CC:DD:FF", -90, true),
|
||||||
|
BluetoothDevice("My fridge", "AA:BB:CC:DD:FF", -100, true),
|
||||||
|
BluetoothDevice("Unknown device", "AA:BB:CC:DD:FF", -130, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Surface {
|
||||||
|
DiscoveredDevicesList(bluetoothState = state, selectedDevice = null, choiceDevice = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,17 +18,17 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import androidx.constraintlayout.compose.Dimension
|
import androidx.constraintlayout.compose.Dimension
|
||||||
import com.helible.pilot.components.Title
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@Composable
|
@Composable
|
||||||
fun BluetoothScannerScreen(
|
fun ScannerScreen(
|
||||||
bluetoothState: BluetoothUiState,
|
bluetoothState: BluetoothUiState,
|
||||||
selectedDevice: BluetoothDevice?,
|
selectedDevice: BluetoothDevice?,
|
||||||
startScan: () -> Unit,
|
startScan: () -> Unit,
|
||||||
@@ -107,3 +107,32 @@ fun BluetoothScannerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ScannerScreenPreview() {
|
||||||
|
val state = BluetoothUiState(
|
||||||
|
pairedBluetoothDevices = listOf(
|
||||||
|
BluetoothDevice("My car", "AA:BB:CC:DD:FF", -70, false),
|
||||||
|
BluetoothDevice("Speaker", "AA:BB:CC:DD:FF", -20, false),
|
||||||
|
BluetoothDevice("My TV", "AA:BB:CC:DD:FF", 10, false),
|
||||||
|
BluetoothDevice("My phone", "AA:BB:CC:DD:FF", -50, false),
|
||||||
|
BluetoothDevice("Mi Band 6", "AA:BB:CC:DD:FF", -100, false),
|
||||||
|
),
|
||||||
|
scannedBluetoothDevices = listOf(
|
||||||
|
BluetoothDevice("Watch", "AA:BB:CC:DD:FF", -10, true),
|
||||||
|
BluetoothDevice("Mi Cleaner", "AA:BB:CC:DD:FF", -90, true),
|
||||||
|
BluetoothDevice("My fridge", "AA:BB:CC:DD:FF", -100, true),
|
||||||
|
BluetoothDevice("Unknown device", "AA:BB:CC:DD:FF", -130, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Surface {
|
||||||
|
ScannerScreen(
|
||||||
|
state,
|
||||||
|
state.scannedBluetoothDevices[1],
|
||||||
|
{}, {},
|
||||||
|
{ _ -> },
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
package com.helible.pilot.components
|
package com.helible.pilot.components.scannerScreen
|
||||||
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Title(text: String, modifier: Modifier = Modifier) {
|
fun Title(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fontSize: TextUnit = 23.sp,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
fontSize = 23.sp,
|
fontSize = fontSize,
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,9 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import com.helible.pilot.BluetoothDataTransferService
|
import com.helible.pilot.viewmodels.BluetoothDataTransferService
|
||||||
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
||||||
import com.helible.pilot.KMessage
|
import com.helible.pilot.dataclasses.GeneralMessage
|
||||||
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
|
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
|
||||||
import com.helible.pilot.receivers.BluetoothStateReceiver
|
import com.helible.pilot.receivers.BluetoothStateReceiver
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -40,7 +40,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
sealed interface ConnectionResult {
|
sealed interface ConnectionResult {
|
||||||
object ConnectionEstablished : ConnectionResult
|
object ConnectionEstablished : ConnectionResult
|
||||||
data class TransferSucceded(val message: KMessage) : ConnectionResult
|
data class TransferSucceded(val message: GeneralMessage) : ConnectionResult
|
||||||
data class Error(val message: String) : ConnectionResult
|
data class Error(val message: String) : ConnectionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ interface BluetoothController {
|
|||||||
fun onDestroy()
|
fun onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
class AndroidBluetoothController(private val context: Context) : BluetoothController {
|
class AndroidBluetoothController(private val context: Context) : BluetoothController {
|
||||||
|
|
||||||
private val bluetoothManager by lazy {
|
private val bluetoothManager by lazy {
|
||||||
@@ -219,6 +220,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
|
|||||||
return flow {}
|
return flow {}
|
||||||
}
|
}
|
||||||
return flow {
|
return flow {
|
||||||
|
Log.i("BluetoothController", "Connecting to device...")
|
||||||
currentClientSocket =
|
currentClientSocket =
|
||||||
bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
|
bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
|
||||||
UUID.fromString(SERVICE_UUID)
|
UUID.fromString(SERVICE_UUID)
|
||||||
@@ -227,19 +229,26 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
|
|||||||
try {
|
try {
|
||||||
socket.connect()
|
socket.connect()
|
||||||
emit(ConnectionResult.ConnectionEstablished)
|
emit(ConnectionResult.ConnectionEstablished)
|
||||||
BluetoothDataTransferService(socket).also { it ->
|
BluetoothDataTransferService(socket).also {
|
||||||
dataTransferService = it
|
dataTransferService = it
|
||||||
emitAll(
|
emitAll(
|
||||||
it.listenForIncomingMessages()
|
it.listenForIncomingMessages()
|
||||||
.map { ConnectionResult.TransferSucceded(it) }
|
.map { deviceState ->
|
||||||
|
ConnectionResult.TransferSucceded(deviceState)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
closeConnection()
|
socket.close()
|
||||||
|
currentClientSocket = null
|
||||||
|
Log.e("BluetoothController", "I/O exception: ${e.message}")
|
||||||
emit(ConnectionResult.Error("Connection was interrupted"))
|
emit(ConnectionResult.Error("Connection was interrupted"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
|
}.onCompletion {
|
||||||
|
Log.i("BluetoothController", "Connection closed on flow completion.")
|
||||||
|
closeConnection()
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun trySendMessage(message: ByteArray): Boolean {
|
override suspend fun trySendMessage(message: ByteArray): Boolean {
|
||||||
@@ -256,6 +265,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
|
|||||||
override fun closeConnection() {
|
override fun closeConnection() {
|
||||||
currentClientSocket?.close()
|
currentClientSocket?.close()
|
||||||
currentClientSocket = null
|
currentClientSocket = null
|
||||||
|
Log.i("BluetoothController", "Connection closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ data class BluetoothUiState(
|
|||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
|
val deviceState: DeviceState? = null
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
|
||||||
|
data class ChangedDeviceStatus(
|
||||||
|
val status: DeviceStatus
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class DeviceState(
|
||||||
|
val status: DeviceStatus = DeviceStatus.ChargeRequired,
|
||||||
|
@Json(name = "charge") val batteryCharge: Int = 10,
|
||||||
|
val flightHeight: Float = 0f,
|
||||||
|
@Json(name = "y") val yaw: Float = 0f,
|
||||||
|
@Json(name = "p") val pitch: Float = 0f,
|
||||||
|
@Json(name = "r") val roll: Float = 0f,
|
||||||
|
@Json(name = "zIn") val zInertial: Float = 0f,
|
||||||
|
val pidSettings: PidSettings? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
enum class DeviceStatus {
|
||||||
|
Idle,
|
||||||
|
IsPreparingForTakeoff,
|
||||||
|
IsFlying,
|
||||||
|
IsBoarding,
|
||||||
|
IsImuCalibration,
|
||||||
|
ChargeRequired;
|
||||||
|
|
||||||
|
fun description(): String {
|
||||||
|
return when (this) {
|
||||||
|
Idle -> "Готово к работе"
|
||||||
|
IsPreparingForTakeoff -> "Подготовка к полёту"
|
||||||
|
IsFlying -> "В полёте"
|
||||||
|
IsBoarding -> "Посадка"
|
||||||
|
IsImuCalibration -> "Калибровка..."
|
||||||
|
ChargeRequired -> "Аккумулятор разряжен"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.FromJson
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
import com.squareup.moshi.ToJson
|
||||||
|
|
||||||
|
class DeviceStatusJsonAdapter {
|
||||||
|
@FromJson
|
||||||
|
fun fromJson(deviceStatus: String): DeviceStatus {
|
||||||
|
try {
|
||||||
|
val index: UInt = deviceStatus.toUInt()
|
||||||
|
return DeviceStatus.values()[index.toInt()]
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
throw JsonDataException("Impossible conversation from String to DeviceStatus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToJson
|
||||||
|
fun toJson(deviceStatus: DeviceStatus): String {
|
||||||
|
return DeviceStatus.values().indexOf(deviceStatus).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
// This dataclass provide message content with its type without any markers
|
||||||
|
data class GeneralMessage(
|
||||||
|
val type: MessageType,
|
||||||
|
val data: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
enum class MessageType {
|
||||||
|
UpdateMessage,
|
||||||
|
PidSettings
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.helible.pilot.dataclasses
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class RotorsSpeedMessage(val r1: Short, val r2: Short, val r3: Short)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class EmergStopMessage(val emergStop: Boolean)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class AlarmStateMessage(val isAlarmOn: Boolean)
|
|
||||||
10
app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt
Normal file
10
app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PidParams (
|
||||||
|
val p: Float,
|
||||||
|
val i: Float,
|
||||||
|
val d: Float
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PidSettingRequiredMessage (
|
||||||
|
val pidSettingOpened: Boolean = true
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PidSettings (
|
||||||
|
@Json(name = "p1") val heightControllerParams: PidParams,
|
||||||
|
@Json(name = "p2") val yawControllerParams: PidParams,
|
||||||
|
@Json(name = "p3") val pitchControllerParams: PidParams
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RotorsDuty(
|
||||||
|
val r1: Short,
|
||||||
|
val r2: Short,
|
||||||
|
val r3: Short
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
data class StickPosition (
|
||||||
|
val x: Float,
|
||||||
|
val y: Float
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
data class SticksPosition(
|
||||||
|
@Json(name = "hS") val heightStick: Int,
|
||||||
|
@Json(name = "yS") val yawStick: Int,
|
||||||
|
@Json(name = "pS") val pitchStick: Int
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class StopMessage(
|
||||||
|
val stop: Boolean = true
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.helible.pilot.exceptions
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class TransferFailedException : IOException("Reading incoming data failed")
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package com.helible.pilot.receivers
|
package com.helible.pilot.receivers
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
class BluetoothAdapterStateReceiver(
|
class BluetoothAdapterStateReceiver(
|
||||||
private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit,
|
private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.util.Log
|
||||||
|
import com.helible.pilot.dataclasses.GeneralMessage
|
||||||
|
import com.helible.pilot.dataclasses.MessageType
|
||||||
|
import com.helible.pilot.exceptions.TransferFailedException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
const val maxPackageSize = 512 // [bytes]
|
||||||
|
|
||||||
|
class BluetoothDataTransferService(
|
||||||
|
private val socket: BluetoothSocket,
|
||||||
|
) {
|
||||||
|
fun listenForIncomingMessages(): Flow<GeneralMessage> {
|
||||||
|
return flow {
|
||||||
|
if (!socket.isConnected)
|
||||||
|
return@flow
|
||||||
|
val buffer = BufferedInputStream(socket.inputStream, maxPackageSize)
|
||||||
|
while (true) {
|
||||||
|
val message: String = try {
|
||||||
|
buffer.bufferedReader().readLine()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("BluetoothController", "Failed to receive incoming data")
|
||||||
|
throw TransferFailedException()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val messageType =
|
||||||
|
MessageType.values().elementAtOrNull(message.split(";").first().toInt())
|
||||||
|
val messageData = message.split(";").last()
|
||||||
|
if (messageType != null) {
|
||||||
|
emit(GeneralMessage(messageType, messageData))
|
||||||
|
}
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
Log.e("BluetoothController", "Message type is invalid: ${e.message}")
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Log.e("BluetoothController", "Message invalid, may be device buffer congested: ${e.message}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BluetoothController", "Unknown error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendMessage(bytes: ByteArray): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
socket.outputStream.write(bytes)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("BluetoothController", "Failed to write message: $e")
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
package com.helible.pilot.viewmodels
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.helible.pilot.dataclasses.AlarmStateMessage
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
|
||||||
import com.helible.pilot.dataclasses.EmergStopMessage
|
|
||||||
import com.helible.pilot.dataclasses.RotorsSpeedMessage
|
|
||||||
import com.helible.pilot.controllers.BluetoothController
|
import com.helible.pilot.controllers.BluetoothController
|
||||||
import com.helible.pilot.controllers.ConnectionResult
|
import com.helible.pilot.controllers.ConnectionResult
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.ChangedDeviceStatus
|
||||||
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
|
||||||
|
import com.helible.pilot.dataclasses.MessageType
|
||||||
|
import com.helible.pilot.dataclasses.PidSettingRequiredMessage
|
||||||
|
import com.helible.pilot.dataclasses.PidSettings
|
||||||
|
import com.helible.pilot.dataclasses.RotorsDuty
|
||||||
|
import com.helible.pilot.dataclasses.SticksPosition
|
||||||
|
import com.helible.pilot.dataclasses.StopMessage
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
import com.squareup.moshi.JsonEncodingException
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -19,12 +30,12 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
|
||||||
|
|
||||||
class BluetoothViewModel(
|
class BluetoothViewModel(
|
||||||
private val bluetoothController: BluetoothController,
|
private val bluetoothController: BluetoothController,
|
||||||
@@ -43,10 +54,31 @@ class BluetoothViewModel(
|
|||||||
pairedBluetoothDevices = pairedDevices
|
pairedBluetoothDevices = pairedDevices
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
||||||
private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
private val _rotorsDuty: MutableStateFlow<RotorsDuty> = MutableStateFlow(RotorsDuty(0, 0, 0))
|
||||||
private val rotorsStateMessegeAdapter = moshi.adapter(RotorsSpeedMessage::class.java)
|
private val _isRotorsTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
private val alarmStateMessageAdapter = moshi.adapter(AlarmStateMessage::class.java)
|
private val _isConsoleTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
private val emergStopMessageAdapter = moshi.adapter(EmergStopMessage::class.java)
|
private val _sticksPosition: MutableStateFlow<SticksPosition> = MutableStateFlow(SticksPosition(0, 0, 0))
|
||||||
|
|
||||||
|
val rotorsDuty: StateFlow<RotorsDuty>
|
||||||
|
get() = _rotorsDuty.asStateFlow()
|
||||||
|
|
||||||
|
private var deviceConnectionJob: Job? = null
|
||||||
|
|
||||||
|
private val moshi =
|
||||||
|
Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build()
|
||||||
|
private val statusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java)
|
||||||
|
private val deviceStateMessageAdapter = moshi.adapter(DeviceState::class.java)
|
||||||
|
private val pidSittingsMessageAdapter = moshi.adapter(PidSettings::class.java)
|
||||||
|
private val pidSittingsRequiredMessageAdapter =
|
||||||
|
moshi.adapter(PidSettingRequiredMessage::class.java)
|
||||||
|
private val rotorDutyMessageAdapter = moshi.adapter(RotorsDuty::class.java)
|
||||||
|
private val stopAllRotorsMessageAdapter = moshi.adapter(StopMessage::class.java)
|
||||||
|
private val consoleStateMessageAdapter = moshi.adapter(SticksPosition::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val messageDelimiter = "\n"
|
||||||
|
const val telemetryPauseDuractionMs: Long = 100
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
bluetoothController.isConnected.onEach { isConnected ->
|
bluetoothController.isConnected.onEach { isConnected ->
|
||||||
@@ -94,7 +126,37 @@ class BluetoothViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionResult.TransferSucceded -> {
|
is ConnectionResult.TransferSucceded -> {
|
||||||
TODO("Telemetry not implemented")
|
try {
|
||||||
|
when (result.message.type) {
|
||||||
|
MessageType.PidSettings -> {
|
||||||
|
val newPidSettings =
|
||||||
|
pidSittingsMessageAdapter.fromJson(result.message.data)
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
deviceState = it.deviceState?.copy(pidSettings = newPidSettings)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.UpdateMessage -> {
|
||||||
|
val newDeviceState =
|
||||||
|
deviceStateMessageAdapter.fromJson(result.message.data)
|
||||||
|
if (newDeviceState != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
deviceState = newDeviceState.copy(pidSettings = it.deviceState?.pidSettings)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: JsonDataException) {
|
||||||
|
Log.e("BluetoothVM", "Failed to parse message: ${result.message.data}")
|
||||||
|
} catch (e: JsonEncodingException) {
|
||||||
|
Log.e("BluetoothVM", "Failed to decode message: ${result.message.data}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BluetoothVM", "Unknown error on message: ${result.message.data}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionResult.Error -> {
|
is ConnectionResult.Error -> {
|
||||||
@@ -108,22 +170,25 @@ class BluetoothViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { _ ->
|
.catch { throwable ->
|
||||||
|
Log.e(
|
||||||
|
"BluetoothVM",
|
||||||
|
"Error occured while data transfer: ${throwable.localizedMessage}"
|
||||||
|
)
|
||||||
bluetoothController.closeConnection()
|
bluetoothController.closeConnection()
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isConnected = false,
|
isConnected = false,
|
||||||
isConnecting = false
|
isConnecting = false,
|
||||||
|
deviceState = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var deviceConnectionJob: Job? = null
|
|
||||||
|
|
||||||
fun connectToDevice(device: String) {
|
fun connectToDevice(device: String) {
|
||||||
if(_state.value.isConnected) {
|
if (_state.value.isConnected or _state.value.isConnecting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_state.update { it.copy(isConnecting = true) }
|
_state.update { it.copy(isConnecting = true) }
|
||||||
@@ -135,10 +200,13 @@ class BluetoothViewModel(
|
|||||||
fun disconnectFromDevice() {
|
fun disconnectFromDevice() {
|
||||||
deviceConnectionJob?.cancel()
|
deviceConnectionJob?.cancel()
|
||||||
bluetoothController.closeConnection()
|
bluetoothController.closeConnection()
|
||||||
|
_isConsoleTelemetryEnabled.update { false }
|
||||||
|
_isRotorsTelemetryEnabled.update { false }
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isConnecting = false,
|
isConnecting = false,
|
||||||
isConnected = false
|
isConnected = false,
|
||||||
|
deviceState = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,36 +230,196 @@ class BluetoothViewModel(
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendRotorsDutySpeed(rotorsState: RotorsSpeedMessage) {
|
fun startImuCalibration() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bluetoothController.trySendMessage(
|
val message = statusMessageAdapter.toJson(
|
||||||
rotorsStateMessegeAdapter.toJson(rotorsState).plus("\r").toByteArray()
|
ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
|
||||||
|
) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(
|
||||||
|
message.toByteArray()
|
||||||
)
|
)
|
||||||
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to start IMU calibration: $message")
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
deviceState = it.deviceState?.copy(status = DeviceStatus.IsImuCalibration)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendAlarmState(alarmStateMessage: AlarmStateMessage) {
|
fun requestPidSettings() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bluetoothController.trySendMessage(
|
val message =
|
||||||
alarmStateMessageAdapter.toJson(alarmStateMessage).plus("\r").toByteArray()
|
pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimiter
|
||||||
|
Log.i("BluetoothVM", "Requested PID settings: $message")
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(
|
||||||
|
message.toByteArray()
|
||||||
)
|
)
|
||||||
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendEmergStop() {
|
fun applyPidSettings(pidSettings: PidSettings) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bluetoothController.trySendMessage(
|
val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimiter
|
||||||
emergStopMessageAdapter.toJson(EmergStopMessage(true)).plus("\r").toByteArray()
|
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||||
)
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||||||
|
_state.update {
|
||||||
|
it.copy(errorMessage = "Не удалось обновить значения PID")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(deviceState = it.deviceState?.copy(pidSettings = pidSettings))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendR3Duty(r3: Int) {
|
fun clearPidSettings() {
|
||||||
|
Log.i("BluetoothVM", "PidSettings cleared")
|
||||||
|
_state.update {
|
||||||
|
it.copy(deviceState = it.deviceState?.copy(pidSettings = null))
|
||||||
|
}
|
||||||
|
Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRotorsDuty() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bluetoothController.trySendMessage(
|
val message = rotorDutyMessageAdapter.toJson(
|
||||||
"R3$r3\n\r".toByteArray()
|
_rotorsDuty.value
|
||||||
|
) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(
|
||||||
|
message.toByteArray()
|
||||||
)
|
)
|
||||||
delay(30)
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to send rotors telemetry: $message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startRotorsConfigurationTelemetry() {
|
||||||
|
Log.i("BluetoothVM", "Start send rotors configuration telemetry...")
|
||||||
|
if(_isRotorsTelemetryEnabled.value) return
|
||||||
|
_isRotorsTelemetryEnabled.update { true }
|
||||||
|
flow {
|
||||||
|
while(_isRotorsTelemetryEnabled.value) {
|
||||||
|
emit(Unit)
|
||||||
|
delay(telemetryPauseDuractionMs)
|
||||||
|
}
|
||||||
|
}.onEach{
|
||||||
|
sendRotorsDuty()
|
||||||
|
Log.d("BluetoothVM", "Sended rotors telemetry")
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRotorsConfigurationTelemetry() {
|
||||||
|
Log.i("BluetoothVM", "Stop send rotors configuration periodically...")
|
||||||
|
_isRotorsTelemetryEnabled.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRotorsDuty(newRotorsDuty: RotorsDuty) {
|
||||||
|
_rotorsDuty.update { newRotorsDuty }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRotors() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val message = stopAllRotorsMessageAdapter.toJson(StopMessage()) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||||
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to stop all rotors: $message")
|
||||||
|
_state.update {
|
||||||
|
it.copy(errorMessage = "Не удалось остановить моторы!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_rotorsDuty.update { RotorsDuty(0, 0, 0) }
|
||||||
|
_isConsoleTelemetryEnabled.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun startTakeoff() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsFlying)) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||||
|
if(!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to start takeoff: $message")
|
||||||
|
_state.update {
|
||||||
|
it.copy(errorMessage = "Не удалось начать полёт!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsFlying)) }
|
||||||
|
startConsoleTelemetrySending()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startOnboarding() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsBoarding)) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||||||
|
if(!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to start onboarding: $message")
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsBoarding)) }
|
||||||
|
stopConsoleTelemetry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeHeightStickPosition(newHeightStickPosition: Int) {
|
||||||
|
_sticksPosition.update {
|
||||||
|
it.copy(heightStick = newHeightStickPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeYawStickPosition(newYawStickPosition: Int) {
|
||||||
|
_sticksPosition.update {
|
||||||
|
it.copy(yawStick = newYawStickPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changePitchStickPosition(newPitchStickPosition: Int) {
|
||||||
|
_sticksPosition.update {
|
||||||
|
it.copy(pitchStick = newPitchStickPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendConsoleState() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val message = consoleStateMessageAdapter.toJson(
|
||||||
|
_sticksPosition.value
|
||||||
|
) + messageDelimiter
|
||||||
|
val isSuccess = bluetoothController.trySendMessage(
|
||||||
|
message.toByteArray()
|
||||||
|
)
|
||||||
|
Log.i("BluetoothVM", "Sended telemetry message: $message")
|
||||||
|
if (!isSuccess) {
|
||||||
|
Log.e("BluetoothVM", "Failed to send console telemetry: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startConsoleTelemetrySending() {
|
||||||
|
if(_isConsoleTelemetryEnabled.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_isConsoleTelemetryEnabled.update { true }
|
||||||
|
flow {
|
||||||
|
while (_isConsoleTelemetryEnabled.value) {
|
||||||
|
emit(Unit)
|
||||||
|
delay(telemetryPauseDuractionMs)
|
||||||
|
}
|
||||||
|
}.onEach {
|
||||||
|
sendConsoleState()
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopConsoleTelemetry() {
|
||||||
|
_isConsoleTelemetryEnabled.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.helible.pilot.controllers.AndroidBluetoothController
|
import com.helible.pilot.controllers.AndroidBluetoothController
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
<vector android:height="24dp"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:tint="#000000"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportHeight="24"
|
||||||
<path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
|
android:viewportWidth="24"
|
||||||
|
android:width="24dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="500" android:viewportHeight="500">
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="500"
|
||||||
|
android:viewportHeight="500">
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:colorPrimary"
|
android:fillColor="?android:colorPrimary"
|
||||||
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
|
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
|
||||||
|
|||||||
10
app/src/main/res/drawable/stick_background.xml
Normal file
10
app/src/main/res/drawable/stick_background.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="210dp"
|
||||||
|
android:height="210dp"
|
||||||
|
android:viewportWidth="210"
|
||||||
|
android:viewportHeight="210">
|
||||||
|
<path
|
||||||
|
android:pathData="M0.5,105.5a105,105 0,1 0,210 0a105,105 0,1 0,-210 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/stick_dot.xml
Normal file
10
app/src/main/res/drawable/stick_dot.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="95dp"
|
||||||
|
android:height="95dp"
|
||||||
|
android:viewportWidth="95"
|
||||||
|
android:viewportHeight="95">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,47.5a47.5,47.5 0,1 0,95 0a47.5,47.5 0,1 0,-95 0z"
|
||||||
|
android:fillColor="#f83a14"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Digital Pilot</string>
|
<string name="app_name">Digital Pilot</string>
|
||||||
|
<string name="calibration_description">Расположите устройство на ровной горизонтальной поверхности, чтобы сани вертолета полностью лежали на ней. Нажмите кнопку калибровки ниже и ждите её окончания, не создавая тряски.</string>
|
||||||
|
<string name="p_pid_value_description">Сначала подберите значение коэффицента P, которое балансирует между слишком низкой и слишком высокой чувствительностью.</string>
|
||||||
|
<string name="i_pid_value_description">Затем подберите значение коэффицента I, которое уберёт нежелательный дрейф, но не повлияет на отзывчивость.</string>
|
||||||
|
<string name="d_pid_value_description">После установите значение коэффицента D таким образом, чтобы обеспечить более стабильное и плавное управление.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.Testblue" parent="android:Theme.Material.Light.NoActionBar" />
|
<style name="Theme.Main" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.helible.pilot
|
package com.helible.pilot
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.1.0" apply false
|
id("com.android.application") version "8.2.2" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
|
id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
|
||||||
}
|
}
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Sun Aug 13 15:00:54 KRAT 2023
|
#Sun Aug 13 15:00:54 KRAT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pluginManagement {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
maven(url = "https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven(url = "https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user