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:
commit
11cf7c2b63
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
30
.metadata
Normal 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
17
README.md
Normal 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
28
analysis_options.yaml
Normal 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
14
android/.gitignore
vendored
Normal 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
|
||||||
198
android/.kotlin/errors/errors-1775992194545.log
Normal file
198
android/.kotlin/errors/errors-1775992194545.log
Normal 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
|
||||||
|
|
||||||
|
|
||||||
44
android/app/build.gradle.kts
Normal file
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
51
android/app/src/main/AndroidManifest.xml
Normal file
51
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.metatroncube.hvbt_dash
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
7
android/app/src/profile/AndroidManifest.xml
Normal 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
24
android/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
4
android/gradle.properties
Normal file
4
android/gradle.properties
Normal 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
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
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
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
27317
android/replay_pid28476.log
Normal file
File diff suppressed because one or more lines are too long
26
android/settings.gradle.kts
Normal file
26
android/settings.gradle.kts
Normal 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")
|
||||||
112
lib/core/bluetooth/bt_poller.dart
Normal file
112
lib/core/bluetooth/bt_poller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/core/bluetooth/bt_service.dart
Normal file
56
lib/core/bluetooth/bt_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/core/models/flag_def.dart
Normal file
7
lib/core/models/flag_def.dart
Normal 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});
|
||||||
|
}
|
||||||
16
lib/core/models/sensor_def.dart
Normal file
16
lib/core/models/sensor_def.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
41
lib/core/models/sensor_defs.dart
Normal file
41
lib/core/models/sensor_defs.dart
Normal 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'),
|
||||||
|
];
|
||||||
249
lib/core/protocol/dtc_map.dart
Normal file
249
lib/core/protocol/dtc_map.dart
Normal 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 (ERR00–ERR03, 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 (ERR00–ERR17, 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;
|
||||||
|
}
|
||||||
142
lib/core/protocol/kpro_parser.dart
Normal file
142
lib/core/protocol/kpro_parser.dart
Normal 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]);
|
||||||
|
|
||||||
|
// AIN0–AIN7: 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];
|
||||||
|
}
|
||||||
22
lib/core/protocol/neg8.dart
Normal file
22
lib/core/protocol/neg8.dart
Normal 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];
|
||||||
|
}
|
||||||
149
lib/core/protocol/s300_parser.dart
Normal file
149
lib/core/protocol/s300_parser.dart
Normal 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();
|
||||||
|
|
||||||
|
// AIN0–AIN7: 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];
|
||||||
|
}
|
||||||
184
lib/core/protocol/sensor_state.dart
Normal file
184
lib/core/protocol/sensor_state.dart
Normal 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 (AIN0–AIN7)
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/core/protocol/temp_table.dart
Normal file
20
lib/core/protocol/temp_table.dart
Normal 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
787
lib/main.dart
Normal 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
626
pubspec.lock
Normal 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
29
pubspec.yaml
Normal 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
69
test/dtc_map_test.dart
Normal 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
133
test/kpro_parser_test.dart
Normal 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
38
test/neg8_test.dart
Normal 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
155
test/s300_parser_test.dart
Normal 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
36
test/temp_table_test.dart
Normal 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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user