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 с тестового проезда.
|
||||
Лучше любого гугла. Гипотеза подтверждена.
|
||||
|
||||
## 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 → НЕСОВМЕСТИМЫ
|
||||
- Web Serial API — не поддерживается на Android вообще
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корень проекта в PYTHONPATH
|
||||
@@ -18,9 +19,13 @@ from elmer.db import Database
|
||||
from elmer.diagnose import Diagnoser
|
||||
from elmer.elm import ELM327
|
||||
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__)
|
||||
config = load()
|
||||
register_raw_endpoint(app)
|
||||
|
||||
|
||||
@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