# Device Files — Drop-in Replacements

These files are production-ready replacements for the Android/Flutter device app.
All scoring logic matches the server (tuning_engine.py, analyze8.py) exactly.

## File Placement

| This File | Goes To | Replaces |
|-----------|---------|----------|
| `scoring_config.json` | `kinometric/assets/scoring_config.json` | YES — replace existing |
| `ScoringConfig.java` | `android/app/src/main/java/com/efsi/kinometric/ScoringConfig.java` | YES — replace existing |
| `BalanceScorer.java` | `android/app/src/main/java/com/efsi/kinometric/BalanceScorer.java` | YES — replace existing |
| `test_duration_check.dart` | `lib/custom_code/actions/test_duration_check.dart` (or wherever Flutter actions live) | NEW — Flutter UI helper |
| `scoring_config_loader.dart` | NOT NEEDED — scoring is in Java, not Dart. Keep for reference only. | N/A |

## What Each File Does

### scoring_config.json (universal — JSON config)
All scoring parameters in one place. Both Java and Python read the same format.
Change values here to adjust scoring without code changes (then recompile).

### ScoringConfig.java (Android native — config parser)
Parses `scoring_config.json` into typed fields. Loaded once at app startup.
```java
// In your activity/service:
String json = loadAsset("scoring_config.json");
ScoringConfig config = new ScoringConfig(json);
```
Has `createDefault()` fallback if JSON loading fails.

### BalanceScorer.java (Android native — scoring engine)
The actual scoring logic. Takes a ScoringConfig and analyzes sensor data.
```java
BalanceScorer scorer = new BalanceScorer(config);

// From CSV content:
Map<String, Object> result = scorer.analyzeFromCsv(csvContent);

// From live sensor arrays (q_diff[] and sensortime[] in ms):
Map<String, Object> result = scorer.analyze(qDiffArray, sensortimeArray);

// Convert to JSON for upload:
String json = BalanceScorer.resultsToJson(result);
```

Result keys:
- `movement_score` (double) — final score 0-10
- `test_duration` (double) — seconds
- `sample_count` (int)
- `windows` — Map with `3_second`, `5_second`, `7_second` sub-maps
- `stability` — Map with `percent_stable`, `continuous_duration`, etc.
- `fatigue` — Map with `pattern` (fatigues/consistent/unstable/declining)
- `duration_penalty` (double) — subtracted from score for short tests
- `seven_second_bonus` (Double) — score cap from 7s bonus logic
- `risk_level` (String) — Critical/High/Moderate/Low
- `scoring_mode` (String) — "qdiff" (production default)

### test_duration_check.dart (Flutter UI — operator warning)
Shows a dialog after recording if the test was too short.
Call from Flutter after recording stops:
```dart
final check = TestDurationCheck.check(csvContent);
if (check.shouldWarn) {
  final retest = await TestDurationCheck.showRetestDialog(context, check);
  if (retest) return; // restart recording
}
// proceed with scoring
```

### scoring_config_loader.dart (NOT NEEDED)
This is a Dart port of the scorer. Since scoring happens in Java (native Android),
this file is redundant. Kept only as reference documentation.

## Scoring Pipeline Summary

1. Parse CSV: skip 3 header rows, read q_diff (col 5) + sensortime (col 7)
2. Skip warmup: first 200 data rows (~2 seconds at 100Hz)
3. Slide windows: find best-scoring 3s and 5s windows
4. Score: piecewise linear interpolation through 6 knot points
5. Aggregate: min of window scores
6. Duration penalty: -3 if only 3s window exists (test too short)
7. 7-second bonus: cap at 7 unless 7s window q-diff meets tier thresholds
8. Output: movement_score, windows, stability, fatigue, risk_level
