android: fork AndrOBD (submodule), thin client (Kotlin), raw-obd endpoint
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "android/AndrOBD"]
|
||||||
|
path = android/AndrOBD
|
||||||
|
url = https://github.com/fr3ts0n/AndrOBD.git
|
||||||
Submodule
+1
Submodule android/AndrOBD added at e056bbcdc6
@@ -0,0 +1,48 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "ru.elmer.client"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "ru.elmer.client"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "0.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// AndrOBD library (forked ELM327 protocol)
|
||||||
|
implementation(project(":library"))
|
||||||
|
|
||||||
|
// Minimal Android
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
|
||||||
|
// HTTP
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
implementation("org.json:json:20231013")
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Только два разрешения -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Elmer">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package ru.elmer.client
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import com.fr3ts0n.ecu.prot.obd.ElmProt
|
||||||
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фоновый сервис: Bluetooth SPP → ELM327 → HTTP-forward.
|
||||||
|
* Забираем сырые OBD-ответы из ElmProt и шлём на сервер.
|
||||||
|
*/
|
||||||
|
class ElmForwardService : Service() {
|
||||||
|
|
||||||
|
private var btSocket: BluetoothSocket? = null
|
||||||
|
private var inputStream: InputStream? = null
|
||||||
|
private var outputStream: OutputStream? = null
|
||||||
|
private val elm = ElmProt()
|
||||||
|
private val okHttp = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private var serverUrl = "https://elmer.kube5s.ru/api/v1/raw-obd"
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var running = false
|
||||||
|
private var readThread: Thread? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ElmForward"
|
||||||
|
const val ACTION_CONNECT = "ru.elmer.client.CONNECT"
|
||||||
|
const val ACTION_DISCONNECT = "ru.elmer.client.DISCONNECT"
|
||||||
|
const val EXTRA_DEVICE_MAC = "device_mac"
|
||||||
|
const val EXTRA_SERVER_URL = "server_url"
|
||||||
|
const val BROADCAST_STATUS = "ru.elmer.client.STATUS"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
intent?.let {
|
||||||
|
it.getStringExtra(EXTRA_SERVER_URL)?.let { url -> serverUrl = url }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
ACTION_CONNECT -> {
|
||||||
|
val mac = intent.getStringExtra(EXTRA_DEVICE_MAC) ?: return START_NOT_STICKY
|
||||||
|
connect(mac)
|
||||||
|
}
|
||||||
|
ACTION_DISCONNECT -> disconnect()
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bluetooth connection ──────────────────────────
|
||||||
|
|
||||||
|
private fun connect(mac: String) {
|
||||||
|
broadcast("Подключение к $mac...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||||
|
val device: BluetoothDevice = adapter.getRemoteDevice(mac)
|
||||||
|
|
||||||
|
// UUID для SPP (Serial Port Profile)
|
||||||
|
val uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
||||||
|
btSocket = device.createRfcommSocketToServiceRecord(uuid)
|
||||||
|
adapter.cancelDiscovery()
|
||||||
|
btSocket?.connect()
|
||||||
|
|
||||||
|
inputStream = btSocket?.inputStream
|
||||||
|
outputStream = btSocket?.outputStream
|
||||||
|
|
||||||
|
// Настраиваем ELM протокол
|
||||||
|
elm.addTelegramWriter { buffer ->
|
||||||
|
outputStream?.write(String(buffer).toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast("Подключено к ELM327")
|
||||||
|
startReading()
|
||||||
|
initElm()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Bluetooth error", e)
|
||||||
|
broadcast("Ошибка: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initElm() {
|
||||||
|
// Отправляем AT-команды инициализации
|
||||||
|
sendRaw("ATZ\r") // сброс
|
||||||
|
Thread.sleep(1000)
|
||||||
|
sendRaw("ATE0\r") // эхо off
|
||||||
|
sendRaw("ATL0\r") // linefeed off
|
||||||
|
sendRaw("ATSP0\r") // авто-протокол
|
||||||
|
sendRaw("ATH1\r") // заголовки on
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reading loop ──────────────────────────────────
|
||||||
|
|
||||||
|
private fun startReading() {
|
||||||
|
running = true
|
||||||
|
readThread = Thread {
|
||||||
|
val buffer = ByteArray(256)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
val bytesRead = inputStream?.read(buffer) ?: -1
|
||||||
|
if (bytesRead == -1) break
|
||||||
|
|
||||||
|
for (i in 0 until bytesRead) {
|
||||||
|
val c = buffer[i].toInt().toChar()
|
||||||
|
if (c == '>' || c == '\r') {
|
||||||
|
if (sb.isNotEmpty()) {
|
||||||
|
val raw = sb.toString().trim()
|
||||||
|
if (raw.isNotEmpty()) {
|
||||||
|
elm.handleTelegram(raw.toCharArray())
|
||||||
|
forwardToServer(raw)
|
||||||
|
}
|
||||||
|
sb.clear()
|
||||||
|
}
|
||||||
|
} else if (c != '\n') {
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (running) {
|
||||||
|
Log.e(TAG, "Read error", e)
|
||||||
|
broadcast("Соединение потеряно")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnect()
|
||||||
|
}.apply {
|
||||||
|
name = "ELM-Reader"
|
||||||
|
isDaemon = true
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast("Чтение данных...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP forward ──────────────────────────────────
|
||||||
|
|
||||||
|
private fun forwardToServer(rawLine: String) {
|
||||||
|
try {
|
||||||
|
val json = JSONObject().apply {
|
||||||
|
put("raw", rawLine)
|
||||||
|
put("timestamp", System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = json.toString()
|
||||||
|
.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(serverUrl)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
okHttp.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
// Молча, сервер недоступен — не критично
|
||||||
|
}
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Forward error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Utils ─────────────────────────────────────────
|
||||||
|
|
||||||
|
fun sendRaw(cmd: String) {
|
||||||
|
try {
|
||||||
|
outputStream?.write(cmd.toByteArray())
|
||||||
|
outputStream?.flush()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Send error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcast(msg: String) {
|
||||||
|
val intent = Intent(BROADCAST_STATUS).apply {
|
||||||
|
putExtra("message", msg)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnect() {
|
||||||
|
running = false
|
||||||
|
try {
|
||||||
|
btSocket?.close()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
broadcast("Отключено")
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
disconnect()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package ru.elmer.client
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тонкий клиент Elmer — одна кнопка.
|
||||||
|
* Подключается к ELM327 по Bluetooth, шлёт сырые OBD-ответы на сервер.
|
||||||
|
*/
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var btnConnect: Button
|
||||||
|
private lateinit var tvStatus: TextView
|
||||||
|
private lateinit var etUrl: EditText
|
||||||
|
|
||||||
|
private val btAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
|
||||||
|
private var elmDevice: BluetoothDevice? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST_BT_PERMISSIONS = 1
|
||||||
|
const val REQUEST_ENABLE_BT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private val statusReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val msg = intent?.getStringExtra("message") ?: return
|
||||||
|
runOnUiThread { tvStatus.text = msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
btnConnect = findViewById(R.id.btn_connect)
|
||||||
|
tvStatus = findViewById(R.id.tv_status)
|
||||||
|
etUrl = findViewById(R.id.et_server_url)
|
||||||
|
|
||||||
|
registerReceiver(statusReceiver, IntentFilter(ElmForwardService.BROADCAST_STATUS),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
|
|
||||||
|
btnConnect.setOnClickListener { startDiagnostics() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDiagnostics() {
|
||||||
|
if (btAdapter == null) {
|
||||||
|
tvStatus.text = "❌ Bluetooth не поддерживается"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка разрешений (Android 12+)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN),
|
||||||
|
REQUEST_BT_PERMISSIONS
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!btAdapter.isEnabled) {
|
||||||
|
startActivityForResult(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем спаренный ELM327 (обычно название содержит "OBD" или "ELM")
|
||||||
|
val paired = btAdapter.bondedDevices
|
||||||
|
elmDevice = paired.find { d ->
|
||||||
|
d.name.uppercase().let { it.contains("OBD") || it.contains("ELM") }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elmDevice == null) {
|
||||||
|
tvStatus.text = "❌ ELM327 не найден. Сопряги устройство в настройках Bluetooth."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, ElmForwardService::class.java).apply {
|
||||||
|
action = ElmForwardService.ACTION_CONNECT
|
||||||
|
putExtra(ElmForwardService.EXTRA_DEVICE_MAC, elmDevice!!.address)
|
||||||
|
putExtra(ElmForwardService.EXTRA_SERVER_URL,
|
||||||
|
etUrl.text.toString().ifBlank { "https://elmer.kube5s.ru/api/v1/raw-obd" })
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(this, intent)
|
||||||
|
tvStatus.text = "⏳ Подключение..."
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterReceiver(statusReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#16213e"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
<stroke android:color="#0f3460" android:width="1dp"/>
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:background="#1a1a2e">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🔧 Elmer"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textColor="#ff6b35"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Диагностика авто через ИИ"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:layout_marginBottom="40dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_server_url"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="URL сервера"
|
||||||
|
android:text="https://elmer.kube5s.ru/api/v1/raw-obd"
|
||||||
|
android:textColor="#e0e0e0"
|
||||||
|
android:textColorHint="#666666"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="@drawable/edit_bg"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:inputType="textUri" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_connect"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🚗 Диагностировать"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="#ffffff"
|
||||||
|
android:backgroundTint="#ff6b35"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Готов к подключению"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#e0e0e0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Elmer</string>
|
||||||
|
<string name="btn_diagnose">🚗 Диагностировать</string>
|
||||||
|
<string name="connecting">Подключение к ELM327…</string>
|
||||||
|
<string name="reading">Чтение данных ЭБУ…</string>
|
||||||
|
<string name="sending">Отправка на сервер…</string>
|
||||||
|
<string name="done">✅ Готово</string>
|
||||||
|
<string name="error_bt">❌ Ошибка Bluetooth</string>
|
||||||
|
<string name="error_server">❌ Ошибка сервера</string>
|
||||||
|
<string name="server_url_hint">URL сервера</string>
|
||||||
|
<string name="server_url_default">https://elmer.kube5s.ru/api/diagnose</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Elmer" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="colorPrimary">#ff6b35</item>
|
||||||
|
<item name="colorPrimaryDark">#1a1a2e</item>
|
||||||
|
<item name="colorAccent">#4ecca3</item>
|
||||||
|
<item name="android:windowBackground">#1a1a2e</item>
|
||||||
|
<item name="android:textColor">#e0e0e0</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Top-level build file
|
||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.2.0" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.9.21" apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "ElmerClient"
|
||||||
|
include(":app")
|
||||||
|
include(":library")
|
||||||
|
|
||||||
|
// AndrOBD library submodule
|
||||||
|
project(":library").projectDir = file("AndrOBD/library")
|
||||||
+36
-1
@@ -12,7 +12,42 @@
|
|||||||
DeepSeek дал полный подробный анализ по логам VCDS с тестового проезда.
|
DeepSeek дал полный подробный анализ по логам VCDS с тестового проезда.
|
||||||
Лучше любого гугла. Гипотеза подтверждена.
|
Лучше любого гугла. Гипотеза подтверждена.
|
||||||
|
|
||||||
## 2026-05-25 — Платформенные ограничения
|
## 2026-05-25 — AndrOBD (fr3ts0n)
|
||||||
|
|
||||||
|
- **Разработчик:** Erwin Scheuch-Heilig (fr3ts0n), Австрия, `erwin.scheuch-heilig@gmx.at`
|
||||||
|
- **Репозиторий:** `github.com/fr3ts0n/AndrOBD`
|
||||||
|
- **Лицензия:** GPLv2
|
||||||
|
- **Год начала:** 2015
|
||||||
|
- **Архитектура:** библиотека (`library/`) + приложение (`androbd/`) + плагины
|
||||||
|
|
||||||
|
### Ключевые файлы для форка
|
||||||
|
|
||||||
|
| Файл | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `library/.../ElmProt.java` | Протокол ELM327: AT-команды, парсинг ответов, DTC, PID, VIN, адаптивные таймауты |
|
||||||
|
| `library/.../ObdProt.java` | OBD2-сервисы, декодирование PID, битовые маски, NRC-коды |
|
||||||
|
| `androbd/.../BtCommService.java` | Bluetooth SPP (классический, не BLE) |
|
||||||
|
| `androbd/.../BleCommService.java` | Bluetooth BLE (запасной вариант) |
|
||||||
|
| `androbd/.../NetworkCommService.java` | WiFi OBD адаптеры |
|
||||||
|
|
||||||
|
### Что НЕ берём
|
||||||
|
|
||||||
|
- `androbd/` — весь UI: графики, дашборды, CSV-экспорт, плагины (MQTT, GPS, сенсоры)
|
||||||
|
- `fastlane/` — метаданные для магазинов
|
||||||
|
|
||||||
|
### Стратегия форка
|
||||||
|
|
||||||
|
- **НЕ копипаст.** Используем Git submodule на AndrOBD
|
||||||
|
- Подключаем только `library/` модуль
|
||||||
|
- Наш код — отдельный `app/` модуль с тонким UI
|
||||||
|
- Лицензия GPLv2 на весь клиент (совместимо с открытостью)
|
||||||
|
- AndrOBD протестирован с 2015 года — экономим недели отладки краевых случаев
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
|
||||||
|
Библиотека AndrOBD (GPLv2) покроет 100% ELM327-клоунов и их глюки.
|
||||||
|
Наш тонкий UI + HTTP-forward — пишем сами.
|
||||||
|
Перед RuStore — форкнуть обязательно. Для MVP (одна машина) — опционально, наш `elm.py` справится.
|
||||||
|
|
||||||
- Chrome Android: Web Bluetooth API — только BLE. ELM327 использует Bluetooth Classic SPP → НЕСОВМЕСТИМЫ
|
- Chrome Android: Web Bluetooth API — только BLE. ELM327 использует Bluetooth Classic SPP → НЕСОВМЕСТИМЫ
|
||||||
- Web Serial API — не поддерживается на Android вообще
|
- Web Serial API — не поддерживается на Android вообще
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Добавляем корень проекта в PYTHONPATH
|
# Добавляем корень проекта в PYTHONPATH
|
||||||
@@ -18,9 +19,13 @@ from elmer.db import Database
|
|||||||
from elmer.diagnose import Diagnoser
|
from elmer.diagnose import Diagnoser
|
||||||
from elmer.elm import ELM327
|
from elmer.elm import ELM327
|
||||||
from elmer.prompts import SYSTEM_PROMPT, build_user_prompt
|
from elmer.prompts import SYSTEM_PROMPT, build_user_prompt
|
||||||
|
from web.raw_endpoint import register as register_raw_endpoint
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
config = load()
|
config = load()
|
||||||
|
register_raw_endpoint(app)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Эндпоинт /api/v1/raw-obd — приём сырых OBD-ответов от тонкого клиента.
|
||||||
|
|
||||||
|
Формат входящих данных:
|
||||||
|
{"raw": "41 05 5A", "timestamp": 1716652800000}
|
||||||
|
|
||||||
|
Сервер логирует, складывает в очередь, и по готовности парсит + отправляет в LLM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from collections import deque
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
logger = logging.getLogger("elmer.raw")
|
||||||
|
|
||||||
|
# Буфер сырых сообщений на сессию (в production — Redis очередь)
|
||||||
|
raw_buffer: deque[dict] = deque()
|
||||||
|
buffer_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
"""Регистрирует эндпоинт на Flask-приложении."""
|
||||||
|
|
||||||
|
@app.route("/api/v1/raw-obd", methods=["POST"])
|
||||||
|
def raw_obd():
|
||||||
|
from flask import request, jsonify
|
||||||
|
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not data or "raw" not in data:
|
||||||
|
return jsonify({"error": "missing 'raw' field"}), 400
|
||||||
|
|
||||||
|
raw = data["raw"].strip()
|
||||||
|
ts = data.get("timestamp", int(datetime.now(timezone.utc).timestamp() * 1000))
|
||||||
|
|
||||||
|
# Логируем в консоль
|
||||||
|
logger.info(f"[RAW] {raw}")
|
||||||
|
|
||||||
|
# Складываем в буфер
|
||||||
|
with buffer_lock:
|
||||||
|
raw_buffer.append({
|
||||||
|
"raw": raw,
|
||||||
|
"timestamp": ts,
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "ok",
|
||||||
|
"received": raw,
|
||||||
|
"buffer_size": len(raw_buffer),
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
Reference in New Issue
Block a user