v1.0.1 — Phase 1 complete + BT connection test UI

- Phase 1 core protocol: temp_table, neg8, sensor_state, s300_parser,
  kpro_parser, dtc_map, sensor_defs (50/50 tests passing)
- BT layer: bt_service.dart, bt_poller.dart (100ms poll, NEG8 validation)
- Connection test UI: device picker, protocol selector, live sensor screen
  with LIVE DATA + DEBUG LOG tabs
- Runtime BT permission request (Android 12+) + auto-enable Bluetooth
- Android: minSdk=26, all BT+location permissions in manifest
- Fixed flutter_bluetooth_serial namespace for AGP compatibility
This commit is contained in:
HVBT Dev 2026-04-12 18:07:37 +05:30
commit 11cf7c2b63
46 changed files with 46526 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Flutter/Dart
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Android
android/.gradle/
android/captures/
android/local.properties
android/key.properties
*.jks
# IDE
.idea/
.vscode/
*.iml
# OS
.DS_Store
Thumbs.db

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# hvbt_dash
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,198 @@
kotlin version: 2.2.20
error message: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in D:\2026\BT-CAR-App\hvbt_dash\build\shared_preferences_android\kotlin\compileReleaseKotlin\cacheable\caches-jvm\jvm\kotlin: class-fq-name-to-source.tab, source-to-classes.tab, internal-name-to-source.tab
at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218)
at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55)
at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:293)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
... 3 more
Caused by: java.lang.Exception: Could not close incremental caches in D:\2026\BT-CAR-App\hvbt_dash\build\shared_preferences_android\kotlin\compileReleaseKotlin\cacheable\caches-jvm\jvm\kotlin: class-fq-name-to-source.tab, source-to-classes.tab, internal-name-to-source.tab
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205)
... 22 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.save(FileToPathConverter.kt:33)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.save(FileToPathConverter.kt:30)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:447)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 24 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:50)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:30)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.hashKey(LinkedCustomHashMap.java:109)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.remove(LinkedCustomHashMap.java:153)
at org.jetbrains.kotlin.com.intellij.util.containers.SLRUMap.remove(SLRUMap.java:89)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.flushAppendCache(PersistentMapImpl.java:1007)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:455)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 24 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.save(FileToPathConverter.kt:33)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.save(FileToPathConverter.kt:30)
at org.jetbrains.kotlin.incremental.storage.AppendableCollectionExternalizer.save(LazyStorage.kt:151)
at org.jetbrains.kotlin.incremental.storage.AppendableCollectionExternalizer.save(LazyStorage.kt:142)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:447)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 24 more
Suppressed: java.lang.Exception: Could not close incremental caches in D:\2026\BT-CAR-App\hvbt_dash\build\shared_preferences_android\kotlin\compileReleaseKotlin\cacheable\caches-jvm\lookups: id-to-file.tab, file-to-id.tab
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155)
... 23 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.LegacyFileExternalizer.save(IdToFileMap.kt:51)
at org.jetbrains.kotlin.incremental.storage.LegacyFileExternalizer.save(IdToFileMap.kt:48)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:447)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 25 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:50)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:30)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.hashKey(LinkedCustomHashMap.java:109)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.remove(LinkedCustomHashMap.java:153)
at org.jetbrains.kotlin.com.intellij.util.containers.SLRUMap.remove(SLRUMap.java:89)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.flushAppendCache(PersistentMapImpl.java:1007)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:455)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 25 more
Suppressed: java.lang.Exception: Could not close incremental caches in D:\2026\BT-CAR-App\hvbt_dash\build\shared_preferences_android\kotlin\compileReleaseKotlin\cacheable\caches-jvm\inputs: source-to-output.tab
... 25 more
Suppressed: java.lang.IllegalArgumentException: this and base files have different roots: C:\Users\mohan\AppData\Local\Pub\Cache\hosted\pub.dev\shared_preferences_android-2.4.23\android\src\main\kotlin\io\flutter\plugins\sharedpreferences\MessagesAsync.g.kt and D:\2026\BT-CAR-App\hvbt_dash\android.
at kotlin.io.FilesKt__UtilsKt.toRelativeString(Utils.kt:117)
at kotlin.io.FilesKt__UtilsKt.relativeTo(Utils.kt:128)
at org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter.toPath(RelocatableFileToPathConverter.kt:24)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:50)
at org.jetbrains.kotlin.incremental.storage.FileDescriptor.getHashCode(FileToPathConverter.kt:30)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.hashKey(LinkedCustomHashMap.java:109)
at org.jetbrains.kotlin.com.intellij.util.containers.LinkedCustomHashMap.remove(LinkedCustomHashMap.java:153)
at org.jetbrains.kotlin.com.intellij.util.containers.SLRUMap.remove(SLRUMap.java:89)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.flushAppendCache(PersistentMapImpl.java:1007)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.doPut(PersistentMapImpl.java:455)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.put(PersistentMapImpl.java:426)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.put(PersistentHashMap.java:106)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
... 24 more

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.metatroncube.hvbt_dash"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.metatroncube.hvbt_dash"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,51 @@
<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.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="hvbt_dash"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.metatroncube.hvbt_dash
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
kotlin.incremental=false
org.gradle.daemon=false

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

12804
android/hs_err_pid1456.log Normal file

File diff suppressed because it is too large Load Diff

2990
android/hs_err_pid28476.log Normal file

File diff suppressed because it is too large Load Diff

27317
android/replay_pid28476.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@ -0,0 +1,112 @@
import 'dart:async';
import 'dart:typed_data';
import '../protocol/neg8.dart';
import 'bt_service.dart';
enum EcuType { s300, kpro }
/// Sends the ECU request byte every [pollingInterval] ms,
/// accumulates raw bytes into 128-byte frames, validates NEG8,
/// and emits valid frames on [frameStream].
class BtPoller {
final BtService _service;
EcuType ecuType;
final Duration pollingInterval;
// S300 request: [0x1B, 0x00, 0xE5]
static final Uint8List _s300Request = Uint8List.fromList([0x1B, 0x00, 0xE5]);
// KPro request: [0x1B, 0x01, 0xE4]
static final Uint8List _kproRequest = Uint8List.fromList([0x1B, 0x01, 0xE4]);
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
Stream<Uint8List> get frameStream => _frameController.stream;
final List<int> _rxBuf = [];
Timer? _pollTimer;
StreamSubscription<Uint8List>? _rxSub;
int droppedFrames = 0;
int validFrames = 0;
BtPoller(
this._service, {
this.ecuType = EcuType.s300,
this.pollingInterval = const Duration(milliseconds: 100),
});
void start() {
_rxBuf.clear();
// Listen to raw bytes from BT and accumulate into 128-byte frames
_rxSub = _service.rawStream.listen(
(Uint8List chunk) {
_rxBuf.addAll(chunk);
_processBuffer();
},
onError: (Object e) {
_frameController.addError(e);
stop();
},
);
// Send request at polling interval
_pollTimer = Timer.periodic(pollingInterval, (_) async {
if (_service.isConnected) {
await _service.write(
ecuType == EcuType.s300 ? _s300Request : _kproRequest,
);
}
});
}
void _processBuffer() {
// Extract as many 128-byte frames as possible.
// Frame start sync: first byte should be 0x1B (header marker).
// If we have a misaligned buffer, scan forward.
while (_rxBuf.length >= 128) {
// Scan for 0x1B frame header
int startIdx = 0;
while (startIdx < _rxBuf.length && _rxBuf[startIdx] != 0x1B) {
startIdx++;
}
// Not enough data after sync byte
if (_rxBuf.length - startIdx < 128) {
// Discard bytes before potential header
if (startIdx > 0) {
_rxBuf.removeRange(0, startIdx);
}
break;
}
final Uint8List frame =
Uint8List.fromList(_rxBuf.sublist(startIdx, startIdx + 128));
if (validateFrame(frame)) {
validFrames++;
_frameController.add(frame);
_rxBuf.removeRange(0, startIdx + 128);
} else {
// Bad checksum skip this byte and try again
droppedFrames++;
_rxBuf.removeRange(0, startIdx + 1);
}
}
}
void stop() {
_pollTimer?.cancel();
_pollTimer = null;
_rxSub?.cancel();
_rxSub = null;
_rxBuf.clear();
}
void dispose() {
stop();
_frameController.close();
}
}

View File

@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
/// Wraps flutter_bluetooth_serial for SPP/RFCOMM Classic BT.
class BtService {
BluetoothConnection? _connection;
final StreamController<Uint8List> _rawFrameController =
StreamController<Uint8List>.broadcast();
bool get isConnected => _connection?.isConnected ?? false;
Stream<Uint8List> get rawStream => _rawFrameController.stream;
Future<List<BluetoothDevice>> getPairedDevices() async {
return FlutterBluetoothSerial.instance.getBondedDevices();
}
Future<void> connect(BluetoothDevice device) async {
if (_connection != null) {
await disconnect();
}
_connection = await BluetoothConnection.toAddress(device.address);
_connection!.input!.listen(
(Uint8List data) {
if (!_rawFrameController.isClosed) {
_rawFrameController.add(data);
}
},
onDone: () {
_rawFrameController.addError('BT connection closed');
},
onError: (Object e) {
_rawFrameController.addError(e);
},
);
}
Future<void> write(Uint8List data) async {
if (_connection?.isConnected ?? false) {
_connection!.output.add(data);
await _connection!.output.allSent;
}
}
Future<void> disconnect() async {
await _connection?.close();
_connection = null;
}
void dispose() {
disconnect();
_rawFrameController.close();
}
}

View File

@ -0,0 +1,7 @@
/// Definition of a boolean flag sensor shown as indicator pills.
class FlagDef {
final String id;
final String displayName;
const FlagDef({required this.id, required this.displayName});
}

View File

@ -0,0 +1,16 @@
/// Definition of a single sensor used for bar gauges, pickers, and graph panels.
class SensorDef {
final String id;
final String displayName;
final String unit;
final double min;
final double max;
const SensorDef({
required this.id,
required this.displayName,
required this.unit,
required this.min,
required this.max,
});
}

View File

@ -0,0 +1,41 @@
import 'sensor_def.dart';
import 'flag_def.dart';
/// Master list of all analog sensor definitions.
const List<SensorDef> sensorDefs = [
SensorDef(id: 'rpm', displayName: 'RPM', unit: 'revs', min: 0, max: 9000),
SensorDef(id: 'vss', displayName: 'Speed', unit: 'km/h', min: 0, max: 280),
SensorDef(id: 'map', displayName: 'Manifold pressure', unit: 'kPa', min: 0, max: 300),
SensorDef(id: 'tps', displayName: 'Throttle pedal', unit: '%', min: 0, max: 100),
SensorDef(id: 'inj', displayName: 'Injector duration', unit: 'ms', min: 0, max: 20),
SensorDef(id: 'ign', displayName: 'Timing advance', unit: 'deg', min: -20, max: 60),
SensorDef(id: 'ect', displayName: 'Coolant temp', unit: '°C', min: -40, max: 150),
SensorDef(id: 'iat', displayName: 'Intake air temp', unit: '°C', min: -40, max: 150),
SensorDef(id: 'bat', displayName: 'Battery voltage', unit: 'V', min: 0, max: 20),
SensorDef(id: 'o2', displayName: 'O2 sensor', unit: 'V', min: 0, max: 1.5),
SensorDef(id: 'gear', displayName: 'Gear', unit: '', min: 1, max: 6),
SensorDef(id: 'eth', displayName: 'Ethanol', unit: '%', min: 0, max: 100),
SensorDef(id: 'pa', displayName: 'Baro pressure', unit: 'kPa', min: 0, max: 120),
SensorDef(id: 'afr', displayName: 'AFR', unit: 'λ', min: 0.5, max: 2.0),
SensorDef(id: 'strim', displayName: 'Short trim', unit: '%', min: -30, max: 30),
SensorDef(id: 'ltrim', displayName: 'Long trim', unit: '%', min: -30, max: 30),
SensorDef(id: 'ain0', displayName: 'Analog input 0', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain1', displayName: 'Analog input 1', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain2', displayName: 'Analog input 2', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain3', displayName: 'Analog input 3', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain4', displayName: 'Analog input 4', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain5', displayName: 'Analog input 5', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain6', displayName: 'Analog input 6', unit: 'V', min: 0, max: 5),
SensorDef(id: 'ain7', displayName: 'Analog input 7', unit: 'V', min: 0, max: 5),
];
/// Master list of boolean flag sensors.
const List<FlagDef> flagDefs = [
FlagDef(id: 'mil', displayName: 'MIL'),
FlagDef(id: 'fuelcut', displayName: 'Fuel Cut'),
FlagDef(id: 'fanout', displayName: 'FAN Out'),
FlagDef(id: 'vtec', displayName: 'VTEC'),
FlagDef(id: 'knock', displayName: 'Knock'),
FlagDef(id: 'revlimit', displayName: 'Rev Limit'),
FlagDef(id: 'launch', displayName: 'Launch'),
];

View File

@ -0,0 +1,249 @@
/// DTC maps for S300 and KPro ECU protocols.
///
/// Key format: 'ERRxx_bity' where xx = byte index (00-based), y = bit number (0=LSB).
/// Value: 'Pxxxx — description'
library;
/// S300 DTC map (ERR00ERR03, 4 bytes × 8 bits = 32 possible codes)
const Map<String, String> s300DtcMap = {
// ERR00 (byte 0x31)
'ERR00_bit0': 'P0130 — O2 Sensor (front)',
'ERR00_bit1': 'P0135 — O2 Sensor Heater (front)',
'ERR00_bit2': 'P0505 — Idle Air Control Valve',
'ERR00_bit3': 'P0108 — MAP Sensor High',
'ERR00_bit4': 'P0107 — MAP Sensor Low',
'ERR00_bit5': 'P0113 — IAT Sensor High',
'ERR00_bit6': 'P0112 — IAT Sensor Low',
'ERR00_bit7': 'P0118 — ECT Sensor High',
// ERR01 (byte 0x32)
'ERR01_bit0': 'P0117 — ECT Sensor Low',
'ERR01_bit1': 'P0336 — CKP Sensor',
'ERR01_bit2': 'P0341 — CMP Sensor',
'ERR01_bit3': 'P1362 — TDC Sensor (No Signal)',
'ERR01_bit4': 'P1363 — TDC Sensor (No Signal 2)',
'ERR01_bit5': 'P0335 — CKP Sensor (No Signal)',
'ERR01_bit6': 'P1361 — TDC Sensor',
'ERR01_bit7': 'P0300 — Random Misfire',
// ERR02 (byte 0x33)
'ERR02_bit0': 'P0301 — Misfire Cylinder 1',
'ERR02_bit1': 'P0302 — Misfire Cylinder 2',
'ERR02_bit2': 'P0303 — Misfire Cylinder 3',
'ERR02_bit3': 'P0304 — Misfire Cylinder 4',
'ERR02_bit4': 'P1259 — VTEC System Malfunction',
'ERR02_bit5': 'P0420 — Catalyst System Low Efficiency',
'ERR02_bit6': 'P0401 — EGR Insufficient Flow',
'ERR02_bit7': 'P1456 — EVAP Leak (large) Fuel Tank Side',
// ERR03 (byte 0x34)
'ERR03_bit0': 'P1457 — EVAP Leak (large) Canister Side',
'ERR03_bit1': 'P0498 — EVAP Canister Vent Shut Valve',
'ERR03_bit2': 'P0497 — EVAP Low Purge Flow',
'ERR03_bit3': 'P0122 — TPS Sensor Low',
'ERR03_bit4': 'P0123 — TPS Sensor High',
'ERR03_bit5': 'P0562 — Battery Voltage Low',
'ERR03_bit6': 'P0563 — Battery Voltage High',
'ERR03_bit7': 'P1607 — PCM Internal Circuit',
};
/// KPro DTC map (ERR00ERR17, 18 bytes × 8 bits = 144 possible codes)
const Map<String, String> kproDtcMap = {
// ERR00
'ERR00_bit0': 'P0130 — O2 Sensor (front)',
'ERR00_bit1': 'P0135 — O2 Sensor Heater (front)',
'ERR00_bit2': 'P0505 — Idle Air Control Valve',
'ERR00_bit3': 'P0108 — MAP Sensor High',
'ERR00_bit4': 'P0107 — MAP Sensor Low',
'ERR00_bit5': 'P0113 — IAT Sensor High',
'ERR00_bit6': 'P0112 — IAT Sensor Low',
'ERR00_bit7': 'P0118 — ECT Sensor High',
// ERR01
'ERR01_bit0': 'P0117 — ECT Sensor Low',
'ERR01_bit1': 'P0336 — CKP Sensor',
'ERR01_bit2': 'P0341 — CMP Sensor',
'ERR01_bit3': 'P1362 — TDC Sensor (No Signal)',
'ERR01_bit4': 'P1363 — TDC Sensor (No Signal 2)',
'ERR01_bit5': 'P0335 — CKP Sensor (No Signal)',
'ERR01_bit6': 'P1361 — TDC Sensor',
'ERR01_bit7': 'P0300 — Random Misfire',
// ERR02
'ERR02_bit0': 'P0301 — Misfire Cylinder 1',
'ERR02_bit1': 'P0302 — Misfire Cylinder 2',
'ERR02_bit2': 'P0303 — Misfire Cylinder 3',
'ERR02_bit3': 'P0304 — Misfire Cylinder 4',
'ERR02_bit4': 'P1259 — VTEC System Malfunction',
'ERR02_bit5': 'P0420 — Catalyst System Low Efficiency',
'ERR02_bit6': 'P0401 — EGR Insufficient Flow',
'ERR02_bit7': 'P1456 — EVAP Leak (large) Fuel Tank Side',
// ERR03
'ERR03_bit0': 'P1457 — EVAP Leak (large) Canister Side',
'ERR03_bit1': 'P0498 — EVAP Canister Vent Shut Valve',
'ERR03_bit2': 'P0497 — EVAP Low Purge Flow',
'ERR03_bit3': 'P0122 — TPS Sensor Low',
'ERR03_bit4': 'P0123 — TPS Sensor High',
'ERR03_bit5': 'P0562 — Battery Voltage Low',
'ERR03_bit6': 'P0563 — Battery Voltage High',
'ERR03_bit7': 'P1607 — PCM Internal Circuit',
// ERR04
'ERR04_bit0': 'P0136 — O2 Sensor (rear)',
'ERR04_bit1': 'P0141 — O2 Sensor Heater (rear)',
'ERR04_bit2': 'P1166 — Air Fuel Ratio (A/F) Sensor Heater',
'ERR04_bit3': 'P1167 — Air Fuel Ratio (A/F) Sensor',
'ERR04_bit4': 'P2195 — A/F Sensor Signal Stuck Lean',
'ERR04_bit5': 'P2196 — A/F Sensor Signal Stuck Rich',
'ERR04_bit6': 'P2237 — A/F Sensor Pump Cell Current Input Low',
'ERR04_bit7': 'P2240 — A/F Sensor Pump Cell Current Input High',
// ERR05
'ERR05_bit0': 'P2243 — A/F Sensor Reference Voltage Low',
'ERR05_bit1': 'P2247 — A/F Sensor Reference Voltage High',
'ERR05_bit2': 'P2251 — A/F Sensor VS Cell Voltage Low',
'ERR05_bit3': 'P2254 — A/F Sensor VS Cell Voltage High',
'ERR05_bit4': 'P0031 — O2 Sensor Heater Current Low (bank 1, sensor 1)',
'ERR05_bit5': 'P0032 — O2 Sensor Heater Current High (bank 1, sensor 1)',
'ERR05_bit6': 'P0037 — O2 Sensor Heater Current Low (bank 1, sensor 2)',
'ERR05_bit7': 'P0038 — O2 Sensor Heater Current High (bank 1, sensor 2)',
// ERR06
'ERR06_bit0': 'P0068 — MAP/MAF vs Throttle Position Correlation',
'ERR06_bit1': 'P0101 — MAF Sensor Range/Performance',
'ERR06_bit2': 'P0102 — MAF Sensor Low Input',
'ERR06_bit3': 'P0103 — MAF Sensor High Input',
'ERR06_bit4': 'P0171 — System Lean (bank 1)',
'ERR06_bit5': 'P0172 — System Rich (bank 1)',
'ERR06_bit6': 'P0174 — System Lean (bank 2)',
'ERR06_bit7': 'P0175 — System Rich (bank 2)',
// ERR07
'ERR07_bit0': 'P0325 — Knock Sensor',
'ERR07_bit1': 'P0330 — Knock Sensor 2',
'ERR07_bit2': 'P0339 — CKP Sensor Intermittent',
'ERR07_bit3': 'P0344 — CMP Sensor Intermittent',
'ERR07_bit4': 'P0365 — CMP Sensor B Circuit',
'ERR07_bit5': 'P0390 — CMP Sensor B Circuit (bank 2)',
'ERR07_bit6': 'P0498 — EVAP Canister Vent Shut Valve Low',
'ERR07_bit7': 'P0499 — EVAP Canister Vent Shut Valve High',
// ERR08
'ERR08_bit0': 'P0600 — Serial Communication Link',
'ERR08_bit1': 'P0601 — Internal Control Module Memory',
'ERR08_bit2': 'P0604 — Internal Control Module RAM',
'ERR08_bit3': 'P0605 — Internal Control Module ROM',
'ERR08_bit4': 'P0657 — Actuator Supply Voltage A Open',
'ERR08_bit5': 'P0685 — ECM/PCM Power Relay Control Open',
'ERR08_bit6': 'P0691 — Fan 1 Control Circuit Low',
'ERR08_bit7': 'P0692 — Fan 1 Control Circuit High',
// ERR09
'ERR09_bit0': 'P0693 — Fan 2 Control Circuit Low',
'ERR09_bit1': 'P0694 — Fan 2 Control Circuit High',
'ERR09_bit2': 'P1454 — EVAP Pressure Sensor Range/Performance',
'ERR09_bit3': 'P1455 — EVAP Leak (small)',
'ERR09_bit4': 'P1508 — IAC Valve Circuit Low',
'ERR09_bit5': 'P1509 — IAC Valve Circuit High',
'ERR09_bit6': 'P1519 — Idle Air Control Valve Stuck',
'ERR09_bit7': 'P1706 — A/T Range Switch Out Of Range',
// ERR10
'ERR10_bit0': 'P2101 — Throttle Actuator Control Motor Range',
'ERR10_bit1': 'P2118 — Throttle Actuator Control Motor Current Range',
'ERR10_bit2': 'P2119 — Throttle Actuator Control Throttle Body Range',
'ERR10_bit3': 'P2122 — Throttle/Pedal Position Sensor D Low Input',
'ERR10_bit4': 'P2123 — Throttle/Pedal Position Sensor D High Input',
'ERR10_bit5': 'P2127 — Throttle/Pedal Position Sensor E Low Input',
'ERR10_bit6': 'P2128 — Throttle/Pedal Position Sensor E High Input',
'ERR10_bit7': 'P2135 — Throttle/Pedal Position Sensor A/B Voltage Correlation',
// ERR11
'ERR11_bit0': 'P2138 — Throttle/Pedal Position Sensor D/E Voltage Correlation',
'ERR11_bit1': 'P2176 — Throttle Actuator Control System Idle Position Not Learned',
'ERR11_bit2': 'P2227 — Barometric Pressure Sensor A Circuit Range',
'ERR11_bit3': 'P2228 — Barometric Pressure Sensor A Circuit Low',
'ERR11_bit4': 'P2229 — Barometric Pressure Sensor A Circuit High',
'ERR11_bit5': 'P2610 — ECM/PCM Internal Engine Off Timer',
'ERR11_bit6': 'P2647 — VTEC System High (bank 1)',
'ERR11_bit7': 'P2648 — VTEC System Low (bank 1)',
// ERR12
'ERR12_bit0': 'P2649 — VTEC System High (bank 2)',
'ERR12_bit1': 'P2650 — VTEC System Low (bank 2)',
'ERR12_bit2': 'P3400 — VTEC System Stuck Off (bank 1)',
'ERR12_bit3': 'P3497 — VTEC System Stuck Off (bank 2)',
'ERR12_bit4': 'U0073 — Control Module Communication Bus Off',
'ERR12_bit5': 'U0100 — Lost Communication With ECM/PCM A',
'ERR12_bit6': 'U0155 — Lost Communication With Instrument Panel Cluster',
'ERR12_bit7': 'U0164 — Lost Communication With HVAC Control Module',
// ERR13
'ERR13_bit0': 'P0011 — Camshaft Position A — Timing Over-Advanced (bank 1)',
'ERR13_bit1': 'P0012 — Camshaft Position A — Timing Over-Retarded (bank 1)',
'ERR13_bit2': 'P0021 — Camshaft Position B — Timing Over-Advanced (bank 1)',
'ERR13_bit3': 'P0022 — Camshaft Position B — Timing Over-Retarded (bank 1)',
'ERR13_bit4': 'P0340 — CMP Sensor A Circuit (bank 1)',
'ERR13_bit5': 'P0345 — CMP Sensor A Circuit (bank 2)',
'ERR13_bit6': 'P0366 — CMP Sensor B Circuit Range/Performance (bank 1)',
'ERR13_bit7': 'P0391 — CMP Sensor B Circuit Range/Performance (bank 2)',
// ERR14
'ERR14_bit0': 'P0016 — Crankshaft/Camshaft Position Correlation (bank 1, sensor A)',
'ERR14_bit1': 'P0017 — Crankshaft/Camshaft Position Correlation (bank 1, sensor B)',
'ERR14_bit2': 'P0018 — Crankshaft/Camshaft Position Correlation (bank 2, sensor A)',
'ERR14_bit3': 'P0019 — Crankshaft/Camshaft Position Correlation (bank 2, sensor B)',
'ERR14_bit4': 'P0008 — Engine Position System (bank 1)',
'ERR14_bit5': 'P0009 — Engine Position System (bank 2)',
'ERR14_bit6': 'P0014 — Camshaft Position B — Timing Over-Advanced (bank 1)',
'ERR14_bit7': 'P0024 — Camshaft Position B — Timing Over-Advanced (bank 2)',
// ERR15
'ERR15_bit0': 'P0087 — Fuel Rail/System Pressure Too Low',
'ERR15_bit1': 'P0088 — Fuel Rail/System Pressure Too High',
'ERR15_bit2': 'P0089 — Fuel Pressure Regulator Performance',
'ERR15_bit3': 'P0090 — Fuel Pressure Regulator Control Circuit',
'ERR15_bit4': 'P0091 — Fuel Pressure Regulator Control Circuit Low',
'ERR15_bit5': 'P0092 — Fuel Pressure Regulator Control Circuit High',
'ERR15_bit6': 'P0201 — Injector Circuit/Open Cylinder 1',
'ERR15_bit7': 'P0202 — Injector Circuit/Open Cylinder 2',
// ERR16
'ERR16_bit0': 'P0203 — Injector Circuit/Open Cylinder 3',
'ERR16_bit1': 'P0204 — Injector Circuit/Open Cylinder 4',
'ERR16_bit2': 'P0261 — Injector Circuit Low Cylinder 1',
'ERR16_bit3': 'P0264 — Injector Circuit Low Cylinder 2',
'ERR16_bit4': 'P0267 — Injector Circuit Low Cylinder 3',
'ERR16_bit5': 'P0270 — Injector Circuit Low Cylinder 4',
'ERR16_bit6': 'P0262 — Injector Circuit High Cylinder 1',
'ERR16_bit7': 'P0265 — Injector Circuit High Cylinder 2',
// ERR17
'ERR17_bit0': 'P0268 — Injector Circuit High Cylinder 3',
'ERR17_bit1': 'P0271 — Injector Circuit High Cylinder 4',
'ERR17_bit2': 'P0351 — Ignition Coil A Primary/Secondary Circuit',
'ERR17_bit3': 'P0352 — Ignition Coil B Primary/Secondary Circuit',
'ERR17_bit4': 'P0353 — Ignition Coil C Primary/Secondary Circuit',
'ERR17_bit5': 'P0354 — Ignition Coil D Primary/Secondary Circuit',
'ERR17_bit6': 'P0627 — Fuel Pump Control Circuit Open',
'ERR17_bit7': 'P0628 — Fuel Pump Control Circuit Low',
};
/// Returns a list of active DTC descriptions from the given raw error bytes
/// using the provided map.
List<String> decodeDtcs(List<int> errBytes, Map<String, String> dtcMap) {
final List<String> active = [];
for (int byteIdx = 0; byteIdx < errBytes.length; byteIdx++) {
final int b = errBytes[byteIdx];
if (b == 0) continue;
for (int bit = 0; bit < 8; bit++) {
if ((b >> bit) & 1 == 1) {
final String key = 'ERR${byteIdx.toString().padLeft(2, '0')}_bit$bit';
final String? desc = dtcMap[key];
if (desc != null) active.add(desc);
}
}
}
return active;
}

View File

@ -0,0 +1,142 @@
import 'dart:typed_data';
import 'sensor_state.dart';
import 'temp_table.dart';
import 'neg8.dart';
/// Parses a 128-byte KPro ECU response frame into a [SensorState].
/// Throws [ArgumentError] if frame length is wrong or NEG8 checksum fails.
SensorState parseKPro(Uint8List frame) {
if (frame.length != 128) {
throw ArgumentError('KPro frame must be 128 bytes, got ${frame.length}');
}
if (!validateFrame(frame)) {
throw ArgumentError('KPro frame NEG8 checksum failed');
}
// RPM: bytes 2..3 raw / 4
final int rpmRaw = _readUint16BE(frame, 2);
final double rpm = rpmRaw / 4.0;
// VSS: byte 4 direct km/h
final double vss = frame[4].toDouble();
// TPS: byte 5 direct %
final double tps = frame[5].toDouble();
// MAP: bytes 6 (MAP1) and 33..34 (MAP2 L+H)
final int map1Raw = frame[6];
final int map2Raw = _readUint16BE(frame, 33);
final double map = (map2Raw != 0) ? map2Raw / 10.0 : map1Raw.toDouble();
// INJ: byte 11 raw
final double inj = frame[11].toDouble();
// IGN: byte 13 raw degrees
final double ign = frame[13].toDouble();
// O2: byte 14 raw
final double o2 = frame[14].toDouble();
// KRtrd: byte 21
final int krtrd = frame[21];
// SW bitmaps
final int sw1 = frame[31];
final int sw2 = frame[32];
// ECT: byte 49
final double ect = (tempXlt[frame[49]] + 40).toDouble();
// IAT: byte 50
final double iat = (tempXlt[frame[50]] + 40).toDouble();
// PA: byte 51
final double pa = frame[51].toDouble();
// BAT: byte 52 raw / 10
final double bat = frame[52] / 10.0;
// STRIM: byte 32 (SW2 shares offset 32, STRIM is byte 20 per spec offset +20)
final double strim = frame[20].toDouble();
// LTRIM not explicitly listed in KPro; use 0
const double ltrim = 0.0;
// ETH not in KPro spec; use 0
const double eth = 0.0;
// AFR: byte 25 (AFCMD at offset +19, then raw)
final double afr = frame[25].toDouble();
// ERR bytes: 18 bytes starting at offset 0x3E = 62
final List<int> errBytes = List<int>.generate(18, (i) => frame[62 + i]);
// AIN0AIN7: bytes 0x52..0x61 (offset 82..97, uint16 BE each)
final double ain0 = _readUint16BE(frame, 82).toDouble();
final double ain1 = _readUint16BE(frame, 84).toDouble();
final double ain2 = _readUint16BE(frame, 86).toDouble();
final double ain3 = _readUint16BE(frame, 88).toDouble();
final double ain4 = _readUint16BE(frame, 90).toDouble();
final double ain5 = _readUint16BE(frame, 92).toDouble();
final double ain6 = _readUint16BE(frame, 94).toDouble();
final double ain7 = _readUint16BE(frame, 96).toDouble();
// Gear not in KPro frame, default 0
const int gear = 0;
// Flags
// SW2 bit2 = MIL
final bool mil = (sw2 & 0x04) != 0;
// SW1 bit6 = FLR (fuel cut relay)
final bool fuelCut = (sw1 & 0x40) != 0;
// SW1 bit0 = FANC
final bool fanOut = (sw1 & 0x01) != 0;
// SW2 bit1..0 = VTS (VTEC)
final bool vtec = (sw2 & 0x03) != 0;
// Knock = KRtrd > 0
final bool knock = krtrd > 0;
// Rev limit not directly mapped in KPro SW; false
const bool revLimit = false;
// Launch not in KPro SW; false
const bool launch = false;
return SensorState(
rpm: rpm,
vss: vss,
map: map,
tps: tps,
inj: inj,
ign: ign,
ect: ect,
iat: iat,
bat: bat,
o2: o2,
gear: gear,
eth: eth,
pa: pa,
afr: afr,
strim: strim,
ltrim: ltrim,
ain0: ain0,
ain1: ain1,
ain2: ain2,
ain3: ain3,
ain4: ain4,
ain5: ain5,
ain6: ain6,
ain7: ain7,
mil: mil,
fuelCut: fuelCut,
fanOut: fanOut,
vtec: vtec,
knock: knock,
revLimit: revLimit,
launch: launch,
errBytes: errBytes,
timestamp: DateTime.now(),
);
}
int _readUint16BE(Uint8List frame, int offset) {
return (frame[offset] << 8) | frame[offset + 1];
}

View File

@ -0,0 +1,22 @@
import 'dart:typed_data';
/// Calculates the NEG8 checksum for a buffer.
/// NEG8: start at 0, subtract each byte, keep lower 8 bits.
/// The result should match the last byte of a valid frame.
int calculateNeg8(Uint8List buf) {
int neg8 = 0;
for (int i = 0; i < buf.length; i++) {
neg8 = (neg8 - buf[i]) & 0xFF;
}
return neg8;
}
/// Returns true if the frame's last byte equals the NEG8 of all preceding bytes.
bool validateFrame(Uint8List frame) {
if (frame.isEmpty) return false;
int neg8 = 0;
for (int i = 0; i < frame.length - 1; i++) {
neg8 = (neg8 - frame[i]) & 0xFF;
}
return neg8 == frame[frame.length - 1];
}

View File

@ -0,0 +1,149 @@
import 'dart:typed_data';
import 'sensor_state.dart';
import 'temp_table.dart';
import 'neg8.dart';
/// Parses a 128-byte S300 ECU response frame into a [SensorState].
/// Throws [ArgumentError] if frame length is wrong or NEG8 checksum fails.
SensorState parseS300(Uint8List frame) {
if (frame.length != 128) {
throw ArgumentError('S300 frame must be 128 bytes, got ${frame.length}');
}
if (!validateFrame(frame)) {
throw ArgumentError('S300 frame NEG8 checksum failed');
}
// RPM: bytes 3..4 uint16 BE, used directly
final int rpmRaw = _readUint16BE(frame, 3);
final double rpm = rpmRaw.toDouble();
// VSS: bytes 5..6
final int vssRaw = _readUint16BE(frame, 5);
final double vss =
(vssRaw < 893 || vssRaw == 0xFFFF) ? 0.0 : 228480.0 / vssRaw;
// MAP: bytes 7..8 raw / 10 = kPa
final int mapRaw = _readUint16BE(frame, 7);
final double map = mapRaw / 10.0;
// TPS: byte 9
final int tpsRaw = frame[9];
final double tps = (tpsRaw < 25) ? 0.0 : (tpsRaw * 51.0) / 46.0;
// INJ: bytes 10..11 raw ms
final int injRaw = _readUint16BE(frame, 10);
final double inj = injRaw.toDouble();
// IGN: byte 12 (raw + 120) / 2
final double ign = (frame[12] + 120) / 2.0;
// O2: byte 16 raw
final double o2 = frame[16].toDouble();
// SW bitmaps
final int sw1 = frame[0x11]; // +17
final int sw2 = frame[0x12]; // +18
final int sw3 = frame[0x13]; // +19
final int sw5 = frame[0x43]; // +67
// Gear: byte 0x27 = +39 decimal... wait, +27 hex = decimal 39
final int gear = frame[0x27];
// Strim: byte 0x29 hex = 41 decimal
final double strim = frame[0x29].toDouble();
// Ltrim: byte 0x2B hex = 43 decimal
final double ltrim = frame[0x2B].toDouble();
// PA: byte 0x2C hex = 44 decimal
final double pa = frame[0x2C].toDouble();
// ECT: byte 0x2D hex = 45 decimal
final double ect = (tempXlt[frame[0x2D]] + 40).toDouble();
// IAT: byte 0x2E hex = 46 decimal
final double iat = (tempXlt[frame[0x2E]] + 40).toDouble();
// BAT: byte 0x30 hex = 48 decimal
final double bat = frame[0x30] * 26.0 / 270.0;
// ERR bytes: 0x31..0x34 (4 bytes for S300)
final List<int> errBytes = [
frame[0x31],
frame[0x32],
frame[0x33],
frame[0x34],
];
// Eth: byte 0x38 hex = 56 decimal
final double eth = frame[0x38].toDouble();
// AFR: byte 0x34 overlaps with ERR03 in the spec use it as raw AFR
final double afr = frame[0x34].toDouble();
// AIN0AIN7: decimal offsets 82..97 (uint16 BE each)
final double ain0 = _readUint16BE(frame, 82).toDouble();
final double ain1 = _readUint16BE(frame, 84).toDouble();
final double ain2 = _readUint16BE(frame, 86).toDouble();
final double ain3 = _readUint16BE(frame, 88).toDouble();
final double ain4 = _readUint16BE(frame, 90).toDouble();
final double ain5 = _readUint16BE(frame, 92).toDouble();
final double ain6 = _readUint16BE(frame, 94).toDouble();
final double ain7 = _readUint16BE(frame, 96).toDouble();
// Flags
// SW1 bit3 = REVL
final bool revLimit = (sw1 & 0x08) != 0;
// SW2 bit5 = MIL
final bool mil = (sw2 & 0x20) != 0;
// SW2 bit2 = Fuel (cut)
final bool fuelCut = (sw2 & 0x04) != 0;
// SW2 bit7 = ALTC fanOut; also SW5 bit0 = FANC
final bool fanOut = (sw2 & 0x80) != 0 || (sw5 & 0x01) != 0;
// SW2 bit1..0 = VTS (VTEC solenoid nonzero = active)
final bool vtec = (sw2 & 0x03) != 0;
// Knock = KRtrd > 0 (byte 13)
final bool knock = frame[13] > 0;
// SW3 bit7 = LnchC
final bool launch = (sw3 & 0x80) != 0;
return SensorState(
rpm: rpm,
vss: vss,
map: map,
tps: tps,
inj: inj,
ign: ign,
ect: ect,
iat: iat,
bat: bat,
o2: o2,
gear: gear,
eth: eth,
pa: pa,
afr: afr,
strim: strim,
ltrim: ltrim,
ain0: ain0,
ain1: ain1,
ain2: ain2,
ain3: ain3,
ain4: ain4,
ain5: ain5,
ain6: ain6,
ain7: ain7,
mil: mil,
fuelCut: fuelCut,
fanOut: fanOut,
vtec: vtec,
knock: knock,
revLimit: revLimit,
launch: launch,
errBytes: errBytes,
timestamp: DateTime.now(),
);
}
int _readUint16BE(Uint8List frame, int offset) {
return (frame[offset] << 8) | frame[offset + 1];
}

View File

@ -0,0 +1,184 @@
/// Fully decoded ECU sensor state. Immutable. Produced by S300 or KPro parser.
class SensorState {
final double rpm;
final double vss;
final double map;
final double tps;
final double inj;
final double ign;
final double ect;
final double iat;
final double bat;
final double o2;
final int gear;
final double eth;
final double pa;
final double afr;
final double strim;
final double ltrim;
// Analog inputs (AIN0AIN7)
final double ain0;
final double ain1;
final double ain2;
final double ain3;
final double ain4;
final double ain5;
final double ain6;
final double ain7;
// Boolean flags
final bool mil;
final bool fuelCut;
final bool fanOut;
final bool vtec;
final bool knock;
final bool revLimit;
final bool launch;
// Raw DTC bytes (ERR00..ERR17 for KPro, ERR00..ERR03 for S300)
final List<int> errBytes;
// Frame timestamp
final DateTime timestamp;
const SensorState({
required this.rpm,
required this.vss,
required this.map,
required this.tps,
required this.inj,
required this.ign,
required this.ect,
required this.iat,
required this.bat,
required this.o2,
required this.gear,
required this.eth,
required this.pa,
required this.afr,
required this.strim,
required this.ltrim,
required this.ain0,
required this.ain1,
required this.ain2,
required this.ain3,
required this.ain4,
required this.ain5,
required this.ain6,
required this.ain7,
required this.mil,
required this.fuelCut,
required this.fanOut,
required this.vtec,
required this.knock,
required this.revLimit,
required this.launch,
required this.errBytes,
required this.timestamp,
});
factory SensorState.zero() => SensorState(
rpm: 0,
vss: 0,
map: 0,
tps: 0,
inj: 0,
ign: 0,
ect: 0,
iat: 0,
bat: 0,
o2: 0,
gear: 0,
eth: 0,
pa: 0,
afr: 0,
strim: 0,
ltrim: 0,
ain0: 0,
ain1: 0,
ain2: 0,
ain3: 0,
ain4: 0,
ain5: 0,
ain6: 0,
ain7: 0,
mil: false,
fuelCut: false,
fanOut: false,
vtec: false,
knock: false,
revLimit: false,
launch: false,
errBytes: const [],
timestamp: DateTime(0),
);
SensorState copyWith({
double? rpm,
double? vss,
double? map,
double? tps,
double? inj,
double? ign,
double? ect,
double? iat,
double? bat,
double? o2,
int? gear,
double? eth,
double? pa,
double? afr,
double? strim,
double? ltrim,
double? ain0,
double? ain1,
double? ain2,
double? ain3,
double? ain4,
double? ain5,
double? ain6,
double? ain7,
bool? mil,
bool? fuelCut,
bool? fanOut,
bool? vtec,
bool? knock,
bool? revLimit,
bool? launch,
List<int>? errBytes,
DateTime? timestamp,
}) {
return SensorState(
rpm: rpm ?? this.rpm,
vss: vss ?? this.vss,
map: map ?? this.map,
tps: tps ?? this.tps,
inj: inj ?? this.inj,
ign: ign ?? this.ign,
ect: ect ?? this.ect,
iat: iat ?? this.iat,
bat: bat ?? this.bat,
o2: o2 ?? this.o2,
gear: gear ?? this.gear,
eth: eth ?? this.eth,
pa: pa ?? this.pa,
afr: afr ?? this.afr,
strim: strim ?? this.strim,
ltrim: ltrim ?? this.ltrim,
ain0: ain0 ?? this.ain0,
ain1: ain1 ?? this.ain1,
ain2: ain2 ?? this.ain2,
ain3: ain3 ?? this.ain3,
ain4: ain4 ?? this.ain4,
ain5: ain5 ?? this.ain5,
ain6: ain6 ?? this.ain6,
ain7: ain7 ?? this.ain7,
mil: mil ?? this.mil,
fuelCut: fuelCut ?? this.fuelCut,
fanOut: fanOut ?? this.fanOut,
vtec: vtec ?? this.vtec,
knock: knock ?? this.knock,
revLimit: revLimit ?? this.revLimit,
launch: launch ?? this.launch,
errBytes: errBytes ?? this.errBytes,
timestamp: timestamp ?? this.timestamp,
);
}
}

View File

@ -0,0 +1,20 @@
// Shared temperature lookup table for S300 and KPro ECU protocols.
// Index = raw ECT/IAT byte value. Decoded temp = tempXlt[raw] + 40 (°C).
const List<int> tempXlt = [
190, 190, 188, 186, 183, 181, 179, 177, 175, 172, 170, 168, 166, 163, 161, 159,
157, 155, 153, 151, 149, 146, 144, 142, 140, 138, 137, 136, 134, 133, 132, 130,
129, 128, 127, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 115,
114, 113, 112, 111, 110, 110, 109, 109, 108, 107, 107, 106, 106, 105, 104, 104,
103, 102, 102, 101, 101, 100, 99, 99, 98, 98, 97, 96, 96, 95, 95, 94,
94, 93, 93, 93, 92, 92, 91, 91, 90, 90, 90, 89, 89, 88, 88, 87,
87, 87, 86, 86, 85, 85, 85, 84, 84, 83, 83, 82, 82, 82, 81, 81,
80, 80, 79, 79, 79, 78, 78, 77, 77, 76, 76, 76, 75, 75, 74, 74,
73, 73, 73, 72, 72, 71, 71, 71, 70, 70, 69, 69, 68, 68, 68, 67,
67, 66, 66, 66, 65, 65, 64, 64, 64, 63, 63, 62, 62, 61, 61, 60,
60, 60, 59, 59, 58, 58, 58, 57, 57, 56, 56, 55, 55, 55, 54, 54,
53, 53, 52, 52, 52, 51, 51, 50, 50, 49, 49, 49, 48, 48, 47, 47,
47, 46, 46, 45, 45, 44, 44, 44, 43, 43, 42, 42, 41, 41, 40, 39,
39, 39, 38, 37, 37, 36, 36, 35, 34, 34, 33, 33, 32, 31, 31, 30,
30, 29, 28, 28, 27, 27, 26, 25, 24, 23, 21, 20, 19, 18, 17, 16,
15, 14, 12, 11, 10, 9, 8, 7, 5, 4, 3, 1, 0, 0, 0, 0,
];

787
lib/main.dart Normal file
View File

@ -0,0 +1,787 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:permission_handler/permission_handler.dart';
import 'core/bluetooth/bt_poller.dart';
import 'core/bluetooth/bt_service.dart';
import 'core/protocol/kpro_parser.dart';
import 'core/protocol/s300_parser.dart';
import 'core/protocol/sensor_state.dart';
void main() {
runApp(const HvbtApp());
}
class HvbtApp extends StatelessWidget {
const HvbtApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'HV BT Dashboard',
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFFF4444),
brightness: Brightness.dark,
),
),
home: const BtPickerScreen(),
debugShowCheckedModeBanner: false,
);
}
}
// Bluetooth Device Picker
class BtPickerScreen extends StatefulWidget {
const BtPickerScreen({super.key});
@override
State<BtPickerScreen> createState() => _BtPickerScreenState();
}
class _BtPickerScreenState extends State<BtPickerScreen> {
final BtService _btService = BtService();
List<BluetoothDevice> _devices = [];
bool _loading = true;
String? _connectingAddress;
String? _error;
@override
void initState() {
super.initState();
_initBluetooth();
}
Future<void> _initBluetooth() async {
setState(() { _loading = true; _error = null; });
// 1. Request all required runtime permissions
final statuses = await [
Permission.bluetoothConnect,
Permission.bluetoothScan,
Permission.locationWhenInUse,
].request();
final denied = statuses.entries
.where((e) => e.value.isDenied || e.value.isPermanentlyDenied)
.map((e) => e.key.toString())
.toList();
if (denied.isNotEmpty) {
setState(() {
_loading = false;
_error = 'Permissions denied: ${denied.join(", ")}\n\n'
'Go to Settings → Apps → HV BT Dashboard → Permissions and allow Bluetooth + Location, then tap Refresh.';
});
return;
}
// 2. Make sure Bluetooth is enabled (shows system dialog to turn it on)
final btState = await FlutterBluetoothSerial.instance.state;
if (btState != BluetoothState.STATE_ON) {
await FlutterBluetoothSerial.instance.requestEnable();
// Give the radio a moment to come up
await Future<void>.delayed(const Duration(seconds: 1));
}
await _loadDevices();
}
Future<void> _loadDevices() async {
setState(() { _loading = true; _error = null; });
try {
final devices = await _btService.getPairedDevices();
setState(() {
_devices = devices;
_loading = false;
});
} catch (e) {
setState(() {
_error = 'Failed to load devices:\n$e\n\nMake sure Bluetooth is ON and tap Refresh.';
_loading = false;
});
}
}
Future<void> _connect(BluetoothDevice device, EcuType ecuType) async {
setState(() => _connectingAddress = device.address);
try {
await _btService.connect(device);
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => LiveDataScreen(
btService: _btService,
deviceName: device.name ?? device.address,
ecuType: ecuType,
),
),
);
} catch (e) {
if (!mounted) return;
setState(() {
_connectingAddress = null;
_error = 'Connection to ${device.name ?? device.address} failed:\n$e\n\nMake sure the device is powered on and paired.';
});
}
}
void _showProtocolDialog(BluetoothDevice device) {
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(device.name ?? device.address),
content: const Text('Select ECU protocol:'),
actions: [
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
_connect(device, EcuType.s300);
},
child: const Text('S300'),
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
_connect(device, EcuType.kpro);
},
child: const Text('KPro'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancel'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('HV BT — Select Device'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loading ? null : _initBluetooth,
tooltip: 'Refresh',
),
],
),
body: Column(
children: [
// Error banner
if (_error != null)
_ErrorBanner(
message: _error!,
onDismiss: () => setState(() => _error = null),
),
Expanded(
child: _loading
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 12),
Text('Loading paired devices…'),
],
),
)
: _devices.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bluetooth_disabled,
size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No paired Bluetooth devices found.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text(
'Go to Android Settings → Bluetooth and pair your ECU module first, then come back and tap Refresh.',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
)
: ListView.separated(
itemCount: _devices.length,
separatorBuilder: (_, __) =>
const Divider(height: 1),
itemBuilder: (context, index) {
final device = _devices[index];
final isConnecting =
_connectingAddress == device.address;
return ListTile(
leading: Icon(
Icons.bluetooth,
color: isConnecting
? Colors.blue
: Colors.grey,
),
title:
Text(device.name ?? 'Unknown Device'),
subtitle: Text(device.address),
trailing: isConnecting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey,
),
onTap: isConnecting
? null
: () => _showProtocolDialog(device),
);
},
),
),
],
),
);
}
}
// Live Data Screen
class LiveDataScreen extends StatefulWidget {
final BtService btService;
final String deviceName;
final EcuType ecuType;
const LiveDataScreen({
super.key,
required this.btService,
required this.deviceName,
required this.ecuType,
});
@override
State<LiveDataScreen> createState() => _LiveDataScreenState();
}
class _LiveDataScreenState extends State<LiveDataScreen> {
late final BtPoller _poller;
StreamSubscription<Uint8List>? _frameSub;
SensorState? _state;
int _frameCount = 0;
int _dropped = 0;
DateTime? _lastFrameTime;
// Debug log last 20 events shown on screen
final List<_LogEntry> _log = [];
@override
void initState() {
super.initState();
_addLog('Connecting to ${widget.deviceName}', LogLevel.info);
_addLog(
'Protocol: ${widget.ecuType == EcuType.s300 ? "S300" : "KPro"}',
LogLevel.info);
_poller = BtPoller(
widget.btService,
ecuType: widget.ecuType,
pollingInterval: const Duration(milliseconds: 100),
);
_poller.start();
_addLog('Poller started — sending requests every 100ms', LogLevel.info);
_frameSub = _poller.frameStream.listen(
(Uint8List frame) {
try {
final SensorState s = widget.ecuType == EcuType.s300
? parseS300(frame)
: parseKPro(frame);
final now = DateTime.now();
if (mounted) {
setState(() {
_state = s;
_frameCount++;
_dropped = _poller.droppedFrames;
_lastFrameTime = now;
});
// Log first frame and then every 100 frames
if (_frameCount == 1) {
_addLog('First frame received! RPM=${s.rpm.toStringAsFixed(0)}',
LogLevel.ok);
} else if (_frameCount % 100 == 0) {
_addLog(
'Frame #$_frameCount RPM=${s.rpm.toStringAsFixed(0)} ECT=${s.ect.toStringAsFixed(1)}°C',
LogLevel.info);
}
}
} catch (e) {
_addLog('Parse error: $e', LogLevel.error);
}
},
onError: (Object e) {
_addLog('BT stream error: $e', LogLevel.error);
},
onDone: () {
_addLog('BT stream closed (device disconnected?)', LogLevel.error);
},
);
}
void _addLog(String msg, LogLevel level) {
if (!mounted) return;
setState(() {
_log.insert(
0,
_LogEntry(
message: msg,
level: level,
time: DateTime.now(),
));
if (_log.length > 20) _log.removeLast();
});
}
@override
void dispose() {
_frameSub?.cancel();
_poller.dispose();
widget.btService.dispose();
super.dispose();
}
Future<void> _disconnect() async {
_poller.stop();
await widget.btService.disconnect();
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const BtPickerScreen()),
);
}
String get _statusText {
if (_frameCount == 0) return 'Waiting for data…';
final ago = _lastFrameTime == null
? '?'
: '${DateTime.now().difference(_lastFrameTime!).inMilliseconds}ms ago';
return 'LIVE • last frame $ago';
}
Color get _statusColor {
if (_frameCount == 0) return Colors.orange;
final ms = _lastFrameTime == null
? 999
: DateTime.now().difference(_lastFrameTime!).inMilliseconds;
return ms < 500 ? Colors.green : Colors.orange;
}
@override
Widget build(BuildContext context) {
final protocol =
widget.ecuType == EcuType.s300 ? 'S300' : 'KPro';
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: const Color(0xFF0A0A0A),
appBar: AppBar(
backgroundColor: const Color(0xFF1A1A1A),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.deviceName,
style: const TextStyle(fontSize: 15)),
Row(
children: [
Icon(_frameCount == 0
? Icons.hourglass_empty
: Icons.circle,
size: 10,
color: _statusColor),
const SizedBox(width: 4),
Text(
'$protocol$_statusText • dropped: $_dropped',
style: const TextStyle(
fontSize: 10, color: Colors.grey),
),
],
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled,
color: Colors.redAccent),
onPressed: _disconnect,
tooltip: 'Disconnect',
),
],
bottom: const TabBar(
tabs: [
Tab(text: 'LIVE DATA'),
Tab(text: 'DEBUG LOG'),
],
),
),
body: TabBarView(
children: [
// Tab 1: Live Sensor Data
_state == null
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
color: Colors.orange),
const SizedBox(height: 20),
const Text('Waiting for ECU response…',
style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text(
'Sending ${protocol} request every 100ms\nMake sure ECU is powered on',
style: const TextStyle(
color: Colors.grey, fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
const Text(
'Check DEBUG LOG tab for details',
style: TextStyle(
color: Colors.orange, fontSize: 13),
),
],
),
)
: _SensorGrid(state: _state!, frameCount: _frameCount),
// Tab 2: Debug Log
_DebugLogView(log: _log),
],
),
),
);
}
}
// Sensor Grid
class _SensorGrid extends StatelessWidget {
final SensorState state;
final int frameCount;
const _SensorGrid({required this.state, required this.frameCount});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_BigValue(
label: 'RPM',
value: state.rpm.toStringAsFixed(0),
unit: 'rpm',
color: _rpmColor(state.rpm),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 6,
children: [
_FlagPill('MIL', state.mil, Colors.amber),
_FlagPill('VTEC', state.vtec, Colors.blue),
_FlagPill('KNOCK', state.knock, Colors.red),
_FlagPill('FUEL CUT', state.fuelCut, Colors.red),
_FlagPill('REV LIM', state.revLimit, Colors.orange),
_FlagPill('FAN', state.fanOut, Colors.green),
_FlagPill('LAUNCH', state.launch, Colors.purple),
],
),
const SizedBox(height: 10),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 6,
crossAxisSpacing: 6,
childAspectRatio: 2.4,
children: [
_SensorTile('VSS', '${state.vss.toStringAsFixed(1)} km/h'),
_SensorTile('MAP', '${state.map.toStringAsFixed(1)} kPa'),
_SensorTile('TPS', '${state.tps.toStringAsFixed(1)} %'),
_SensorTile('ECT', '${state.ect.toStringAsFixed(1)} °C'),
_SensorTile('IAT', '${state.iat.toStringAsFixed(1)} °C'),
_SensorTile('BAT', '${state.bat.toStringAsFixed(2)} V'),
_SensorTile('IGN', '${state.ign.toStringAsFixed(1)} °'),
_SensorTile('INJ', '${state.inj.toStringAsFixed(2)} ms'),
_SensorTile('O2', state.o2.toStringAsFixed(0)),
_SensorTile('AFR', state.afr.toStringAsFixed(2)),
_SensorTile('ETH', '${state.eth.toStringAsFixed(0)} %'),
_SensorTile('GEAR', state.gear.toString()),
],
),
const SizedBox(height: 8),
Text(
'Frames received: $frameCount',
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
);
}
Color _rpmColor(double rpm) {
if (rpm > 7000) return Colors.red;
if (rpm > 5500) return Colors.orange;
return const Color(0xFF00E676);
}
}
// Debug Log View
class _DebugLogView extends StatelessWidget {
final List<_LogEntry> log;
const _DebugLogView({required this.log});
@override
Widget build(BuildContext context) {
if (log.isEmpty) {
return const Center(
child: Text('No log entries yet.',
style: TextStyle(color: Colors.grey)),
);
}
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: log.length,
itemBuilder: (context, index) {
final entry = log[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_ts(entry.time),
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
fontFamily: 'monospace'),
),
const SizedBox(width: 8),
Icon(
entry.level == LogLevel.error
? Icons.error_outline
: entry.level == LogLevel.ok
? Icons.check_circle_outline
: Icons.info_outline,
size: 14,
color: entry.level == LogLevel.error
? Colors.red
: entry.level == LogLevel.ok
? Colors.green
: Colors.grey,
),
const SizedBox(width: 6),
Expanded(
child: Text(
entry.message,
style: TextStyle(
fontSize: 12,
color: entry.level == LogLevel.error
? Colors.redAccent
: entry.level == LogLevel.ok
? Colors.greenAccent
: Colors.white70,
),
),
),
],
),
);
},
);
}
String _ts(DateTime t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}';
}
enum LogLevel { info, ok, error }
class _LogEntry {
final String message;
final LogLevel level;
final DateTime time;
const _LogEntry(
{required this.message, required this.level, required this.time});
}
// Error Banner
class _ErrorBanner extends StatelessWidget {
final String message;
final VoidCallback onDismiss;
const _ErrorBanner({required this.message, required this.onDismiss});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: const Color(0xFF4A0000),
padding: const EdgeInsets.fromLTRB(16, 12, 8, 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.error_outline,
color: Colors.redAccent, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
message,
style: const TextStyle(
color: Colors.redAccent, fontSize: 13),
),
),
IconButton(
icon: const Icon(Icons.close,
color: Colors.redAccent, size: 20),
onPressed: onDismiss,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
);
}
}
// UI helpers
class _BigValue extends StatelessWidget {
final String label;
final String value;
final String unit;
final Color color;
const _BigValue(
{required this.label,
required this.value,
required this.unit,
required this.color});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 24),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withAlpha(120), width: 1.5),
),
child: Column(
children: [
Text(label,
style: TextStyle(
color: color,
fontSize: 12,
letterSpacing: 2,
fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(value,
style: TextStyle(
color: color,
fontSize: 48,
fontWeight: FontWeight.bold,
letterSpacing: -2)),
Text(unit,
style: TextStyle(
color: color.withAlpha(160), fontSize: 12)),
],
),
);
}
}
class _SensorTile extends StatelessWidget {
final String label;
final String value;
const _SensorTile(this.label, this.value);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
letterSpacing: 1)),
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600)),
],
),
);
}
}
class _FlagPill extends StatelessWidget {
final String label;
final bool active;
final Color color;
const _FlagPill(this.label, this.active, this.color);
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 120),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: active ? color.withAlpha(40) : const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: active ? color : Colors.grey.withAlpha(60),
width: 1.5,
),
),
child: Text(
label,
style: TextStyle(
color: active ? color : Colors.grey,
fontSize: 11,
fontWeight: active ? FontWeight.bold : FontWeight.normal,
letterSpacing: 1,
),
),
);
}
}

626
pubspec.lock Normal file
View File

@ -0,0 +1,626 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb
url: "https://pub.dev"
source: hosted
version: "0.68.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bluetooth_serial:
dependency: "direct main"
description:
name: flutter_bluetooth_serial
sha256: "85ae82c4099b2b1facdc54e75e1bcfa88dc7f719e55dc886bb0b648cb16636b1"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
url: "https://pub.dev"
source: hosted
version: "1.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
source: hosted
version: "11.4.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
source: hosted
version: "12.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

29
pubspec.yaml Normal file
View File

@ -0,0 +1,29 @@
name: hvbt_dash
description: HV BT Automotive ECU Dashboard
publish_to: 'none'
version: 1.0.1+2
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.0
flutter_bluetooth_serial: ^0.4.0
fl_chart: ^0.68.0
sqflite: ^2.3.0
shared_preferences: ^2.2.0
path_provider: ^2.1.0
intl: ^0.19.0
permission_handler: ^11.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

69
test/dtc_map_test.dart Normal file
View File

@ -0,0 +1,69 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hvbt_dash/core/protocol/dtc_map.dart';
void main() {
group('S300 DTC map', () {
test('contains expected number of entries', () {
expect(s300DtcMap.length, equals(32)); // 4 bytes × 8 bits
});
test('P0130 maps to correct description for ERR00 bit0', () {
expect(s300DtcMap['ERR00_bit0'], equals('P0130 — O2 Sensor (front)'));
});
test('P0505 maps for ERR00 bit2', () {
expect(s300DtcMap['ERR00_bit2'], equals('P0505 — Idle Air Control Valve'));
});
test('P1607 maps for ERR03 bit7', () {
expect(s300DtcMap['ERR03_bit7'], equals('P1607 — PCM Internal Circuit'));
});
});
group('KPro DTC map', () {
test('contains entries for 18 bytes (ERR00..ERR17)', () {
// Every ERR byte has 8 bits = 144 entries total
expect(kproDtcMap.length, equals(144));
});
test('P0130 maps for ERR00 bit0', () {
expect(kproDtcMap['ERR00_bit0'], equals('P0130 — O2 Sensor (front)'));
});
test('ERR17 entries present', () {
expect(kproDtcMap.containsKey('ERR17_bit0'), isTrue);
expect(kproDtcMap.containsKey('ERR17_bit7'), isTrue);
});
});
group('decodeDtcs helper', () {
test('returns empty list when no errors', () {
final result = decodeDtcs([0, 0, 0, 0], s300DtcMap);
expect(result, isEmpty);
});
test('decodes ERR00 bit0 = P0130', () {
final result = decodeDtcs([0x01, 0, 0, 0], s300DtcMap);
expect(result, contains('P0130 — O2 Sensor (front)'));
});
test('decodes multiple bits in one byte', () {
// ERR00 bits 0 and 1
final result = decodeDtcs([0x03, 0, 0, 0], s300DtcMap);
expect(result.length, equals(2));
});
test('decodes bits across multiple bytes', () {
// ERR00 bit0 + ERR01 bit0
final result = decodeDtcs([0x01, 0x01, 0, 0], s300DtcMap);
expect(result.length, equals(2));
});
test('skips bits not in map gracefully', () {
// Use a small dtcMap with only one entry, but set all bits
final result = decodeDtcs([0xFF, 0, 0, 0], {'ERR00_bit0': 'P0130 — test'});
// Only the one mapped bit should appear
expect(result.length, equals(1));
});
});
}

133
test/kpro_parser_test.dart Normal file
View File

@ -0,0 +1,133 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hvbt_dash/core/protocol/kpro_parser.dart';
import 'package:hvbt_dash/core/protocol/temp_table.dart';
/// Builds a zeroed 128-byte KPro frame and stamps the NEG8 checksum at byte 127.
Uint8List _buildKProFrame({
int rpm = 0,
int vss = 0,
int tps = 0,
int map1 = 0,
int map2 = 0,
int ign = 0,
int ect = 0x80, // tempXlt[0x80]=73 113°C
int bat = 0,
int sw1 = 0,
int sw2 = 0,
int krtrd = 0,
}) {
final frame = Uint8List(128);
// Header
frame[0] = 0x1B;
frame[1] = 0x01;
// RPM: bytes 2..3
frame[2] = (rpm >> 8) & 0xFF;
frame[3] = rpm & 0xFF;
// VSS: byte 4
frame[4] = vss & 0xFF;
// TPS: byte 5
frame[5] = tps & 0xFF;
// MAP1: byte 6
frame[6] = map1 & 0xFF;
// MAP2: bytes 33..34
frame[33] = (map2 >> 8) & 0xFF;
frame[34] = map2 & 0xFF;
// IGN: byte 13
frame[13] = ign;
// KRtrd: byte 21
frame[21] = krtrd;
// SW1: byte 31, SW2: byte 32
frame[31] = sw1;
frame[32] = sw2;
// ECT: byte 49
frame[49] = ect;
// BAT: byte 52
frame[52] = bat;
// Stamp NEG8 checksum at byte 127
int neg8 = 0;
for (int i = 0; i < 127; i++) {
neg8 = (neg8 - frame[i]) & 0xFF;
}
frame[127] = neg8;
return frame;
}
void main() {
group('KPro parser', () {
test('parses RPM = raw / 4', () {
// rpm raw = 4000 1000.0 revs
final frame = _buildKProFrame(rpm: 4000);
final state = parseKPro(frame);
expect(state.rpm, equals(1000.0));
});
test('parses VSS directly', () {
final frame = _buildKProFrame(vss: 120);
final state = parseKPro(frame);
expect(state.vss, equals(120.0));
});
test('parses TPS directly', () {
final frame = _buildKProFrame(tps: 75);
final state = parseKPro(frame);
expect(state.tps, equals(75.0));
});
test('parses MAP from MAP1 when MAP2 = 0', () {
final frame = _buildKProFrame(map1: 100, map2: 0);
final state = parseKPro(frame);
expect(state.map, equals(100.0));
});
test('parses MAP from MAP2 / 10 when MAP2 != 0', () {
// map2 raw = 1000 100.0 kPa
final frame = _buildKProFrame(map1: 50, map2: 1000);
final state = parseKPro(frame);
expect(state.map, equals(100.0));
});
test('parses ECT using temp lookup table', () {
// ect raw = 0x80 = 128 tempXlt[128]=73 113°C
final frame = _buildKProFrame(ect: 0x80);
final state = parseKPro(frame);
expect(state.ect, equals((tempXlt[0x80] + 40).toDouble()));
});
test('parses BAT = raw / 10', () {
// bat raw = 130 13.0 V
final frame = _buildKProFrame(bat: 130);
final state = parseKPro(frame);
expect(state.bat, equals(13.0));
});
test('MIL flag from SW2 bit2', () {
final frame = _buildKProFrame(sw2: 0x04); // bit2
final state = parseKPro(frame);
expect(state.mil, isTrue);
});
test('knock flag when krtrd > 0', () {
final frame = _buildKProFrame(krtrd: 3);
final state = parseKPro(frame);
expect(state.knock, isTrue);
});
test('errBytes has 18 entries', () {
final frame = _buildKProFrame();
final state = parseKPro(frame);
expect(state.errBytes.length, equals(18));
});
test('throws on wrong frame length', () {
expect(() => parseKPro(Uint8List(64)), throwsArgumentError);
});
test('throws on bad checksum', () {
final frame = _buildKProFrame(rpm: 4000);
frame[127] = 0x00; // corrupt
expect(() => parseKPro(frame), throwsArgumentError);
});
});
}

38
test/neg8_test.dart Normal file
View File

@ -0,0 +1,38 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hvbt_dash/core/protocol/neg8.dart';
void main() {
group('NEG8 checksum', () {
test('S300 request header [0x1B, 0x00] produces 0xE5', () {
// NEG8 of [0x1B, 0x00] = (0 - 0x1B - 0x00) & 0xFF = 0xE5
final buf = Uint8List.fromList([0x1B, 0x00]);
expect(calculateNeg8(buf), equals(0xE5));
});
test('KPro request header [0x1B, 0x01] produces 0xE4', () {
final buf = Uint8List.fromList([0x1B, 0x01]);
expect(calculateNeg8(buf), equals(0xE4));
});
test('validateFrame passes for correctly checksummed frame', () {
// Build a 4-byte frame where last byte = NEG8 of first 3
final data = Uint8List.fromList([0x1B, 0x00, 0x00, 0xE5]);
expect(validateFrame(data), isTrue);
});
test('validateFrame fails for corrupted frame', () {
final data = Uint8List.fromList([0x1B, 0x00, 0x00, 0xFF]);
expect(validateFrame(data), isFalse);
});
test('validateFrame returns false for empty frame', () {
expect(validateFrame(Uint8List(0)), isFalse);
});
test('single byte 0x00 validates as [0x00, 0x00]', () {
// NEG8([0x00]) = 0x00, so frame [0x00, 0x00] should pass
expect(validateFrame(Uint8List.fromList([0x00, 0x00])), isTrue);
});
});
}

155
test/s300_parser_test.dart Normal file
View File

@ -0,0 +1,155 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hvbt_dash/core/protocol/s300_parser.dart';
import 'package:hvbt_dash/core/protocol/temp_table.dart';
/// Builds a zeroed 128-byte S300 frame and stamps the NEG8 checksum at byte 127.
Uint8List _buildS300Frame({
int rpm = 0,
int vss = 0,
int mapRaw = 0,
int tps = 0,
int inj = 0,
int ign = 0,
int ect = 0x80, // tempXlt[0x80]=73 73+40=113°C
int bat = 0,
int sw1 = 0,
int sw2 = 0,
int sw3 = 0,
int krtrd = 0,
}) {
final frame = Uint8List(128);
// Header
frame[0] = 0x1B;
frame[1] = 0x00;
// RPM: bytes 3..4
frame[3] = (rpm >> 8) & 0xFF;
frame[4] = rpm & 0xFF;
// VSS: bytes 5..6
frame[5] = (vss >> 8) & 0xFF;
frame[6] = vss & 0xFF;
// MAP: bytes 7..8
frame[7] = (mapRaw >> 8) & 0xFF;
frame[8] = mapRaw & 0xFF;
// TPS: byte 9
frame[9] = tps;
// INJ: bytes 10..11
frame[10] = (inj >> 8) & 0xFF;
frame[11] = inj & 0xFF;
// IGN: byte 12
frame[12] = ign;
// KRtrd: byte 13
frame[13] = krtrd;
// SW bitmaps
frame[0x11] = sw1;
frame[0x12] = sw2;
frame[0x13] = sw3;
// ECT: byte 0x2D
frame[0x2D] = ect;
// BAT: byte 0x30
frame[0x30] = bat;
// Stamp NEG8 checksum at byte 127
int neg8 = 0;
for (int i = 0; i < 127; i++) {
neg8 = (neg8 - frame[i]) & 0xFF;
}
frame[127] = neg8;
return frame;
}
void main() {
group('S300 parser', () {
test('parses RPM correctly', () {
// RPM raw = 3000
final frame = _buildS300Frame(rpm: 3000);
final state = parseS300(frame);
expect(state.rpm, equals(3000.0));
});
test('parses VSS = 0 when raw < 893', () {
final frame = _buildS300Frame(vss: 500);
final state = parseS300(frame);
expect(state.vss, equals(0.0));
});
test('parses VSS correctly when raw >= 893', () {
// vss = 228480 / 1000 = 228.48 km/h
final frame = _buildS300Frame(vss: 1000);
final state = parseS300(frame);
expect(state.vss, closeTo(228.48, 0.01));
});
test('parses MAP = raw / 10', () {
// mapRaw = 1000 100.0 kPa
final frame = _buildS300Frame(mapRaw: 1000);
final state = parseS300(frame);
expect(state.map, equals(100.0));
});
test('parses TPS = 0 when raw < 25', () {
final frame = _buildS300Frame(tps: 10);
final state = parseS300(frame);
expect(state.tps, equals(0.0));
});
test('parses TPS correctly when raw >= 25', () {
// tps raw=46 46*51/46 = 51.0
final frame = _buildS300Frame(tps: 46);
final state = parseS300(frame);
expect(state.tps, closeTo(51.0, 0.01));
});
test('parses ECT using temp lookup table', () {
// ect raw = 0x80 = 128 tempXlt[128]=73 73+40=113°C
final frame = _buildS300Frame(ect: 0x80);
final state = parseS300(frame);
expect(state.ect, equals((tempXlt[0x80] + 40).toDouble()));
});
test('parses BAT = raw * 26 / 270', () {
// bat raw = 135 135*26/270 = 13.0 V
final frame = _buildS300Frame(bat: 135);
final state = parseS300(frame);
expect(state.bat, closeTo(13.0, 0.01));
});
test('IGN decode: (raw + 120) / 2', () {
// ign raw = 20 (20+120)/2 = 70.0°
final frame = _buildS300Frame(ign: 20);
final state = parseS300(frame);
expect(state.ign, equals(70.0));
});
test('MIL flag set from SW2 bit5', () {
final frame = _buildS300Frame(sw2: 0x20); // bit5 set
final state = parseS300(frame);
expect(state.mil, isTrue);
});
test('knock flag set when krtrd > 0', () {
final frame = _buildS300Frame(krtrd: 5);
final state = parseS300(frame);
expect(state.knock, isTrue);
});
test('knock flag clear when krtrd = 0', () {
final frame = _buildS300Frame(krtrd: 0);
final state = parseS300(frame);
expect(state.knock, isFalse);
});
test('throws on wrong frame length', () {
expect(
() => parseS300(Uint8List(64)),
throwsArgumentError,
);
});
test('throws on bad checksum', () {
final frame = _buildS300Frame(rpm: 1000);
frame[127] = 0x00; // corrupt checksum
expect(() => parseS300(frame), throwsArgumentError);
});
});
}

36
test/temp_table_test.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hvbt_dash/core/protocol/temp_table.dart';
double decodeTemp(int raw) => (tempXlt[raw] + 40).toDouble();
void main() {
group('Temperature lookup table', () {
test('tempXlt has exactly 256 entries', () {
expect(tempXlt.length, equals(256));
});
test('decodeTemp(0x00) = tempXlt[0] + 40 = 190 + 40 = 230°C', () {
expect(decodeTemp(0x00), equals(230.0));
});
test('decodeTemp(0xA0) = tempXlt[160] + 40', () {
// 0xA0 = 160 decimal; tempXlt[160] = 60
expect(decodeTemp(0xA0), equals(60 + 40.0));
});
test('decodeTemp(0xFF) = tempXlt[255] + 40 = 0 + 40 = 40°C', () {
expect(decodeTemp(0xFF), equals(40.0));
});
test('decodeTemp(0x80) = tempXlt[128] + 40', () {
// 0x80 = 128; tempXlt[128] = 73
expect(decodeTemp(0x80), equals(73 + 40.0));
});
test('all table values are non-negative', () {
for (final v in tempXlt) {
expect(v, greaterThanOrEqualTo(0));
}
});
});
}