Stand OpenBikeSensor Lite

Den Code der App oder der Lite-FW kann ich nicht nachvollziehen. Weiss also nicht, wie das mit der Lenkerbreite dann im Endeffekt als Messung abgespeichert wird.

@Entwickler*innen des Lite und der App. Ich will euch keine Vorwürfe machen oder eure Arbeit schlecht. Es geht mir einzig um Tests der Beta-Version. Die nur dank euch überhaupt verfügbar ist. Merci dafür!

ich hatte auch JSN-SR04T v3.3 angeschlossen, inkl. der Sensoren, die im Lite verbaut werden. Es wurde nichts gemessen. Allenfalls lag es aber an mir - habe die Sensoren in beide Richtungen eingesteckt - wegen Plus und Minus Durcheinenander in meinem Kopf…



Die JSN-SR04T-V3.3 funktionieren bei mir nicht. Weder die Variante mit dem Sensor auf dem Board, noch die mit dem über Koax-angebundenen Sensor, wie sich gerade herausgestellt hat. Sensor mit nicht gekürztem Kabel verwendet. Bei beiden Varianten liegt die Aktualisierungsrate des linken Messwerts in der SimRa-App im Sekundenbereich (!). Bei JSN-SR04-T-V3.0 sieht die Aktualisierungsrate in der App gut aus, der angezeigte Messwert verändert sich ohne wahrnehmbare Verzögerung, wenn sich der Abstand zwischen Sensor und in diesem Fall Wand - ändert. Insofern gehe ich nicht davon aus, dass es ein Problem der App ist.
Da auf den Sensorboards steht, dass diese mit 5 V arbeiten und 5 V am VIN Pin des Developer Board zur Verfügung stehen, habe ich mal versucht, ob es einen Unterschied macht, die Sensoren mit 3,3 oder 5 V zu betreiben - scheinbar nicht. Bei V3.0 funktioniert beides, bei V3.3 beides nicht.
Edit: Das ist auf die Schnelle ausprobiert (Trial / Error) und nicht gemessen. Denke man muss die Sensoren bzw. die OBS unter reproduzierbaren (Labor-) Bedingungen testen, um sicher zu sein, dass diese zuverlässig messen.

1 „Gefällt mir“

Gibt es Neuigkeiten in Sachen Entwicklungen beim OBS Light? Mir fehlen sie Skill, um selbst etwas dazu beizutragen.
Und ich habe das Gerät nicht mehr verwendet.

Hallo zusammen

Ich habe ein LLM auf die Probleme mit der SimRa-App angesetzt. Und es ist allenfalls gelöst, jedenfalls scheint es in der angepassten SimRa-App, die ich nun verwendet habe, zu funktionieren.

Es gabe diese Probleme,

  1. Die eingestellte Lenkerbreite (links) wurde in der Live-Ansicht falsch verwendet.
  2. Die eingestellte Lenkerbreite (links) wurde im Event falsch verwendet.
  3. Einstellung der Lenkerbreite (rechts) hatte den gleichen Einfluss wie die linke Seite, siehe OBS-lite distance reading calculation errors · Issue #35 · simra-project/simra-android · GitHub

Ich habe dann dem LML gesagt, dass die Anpassungen zusammengefasst werden sollen, damit es nachvollziehbar ist. Et voila:

OBSLiteActivity.kt

a) Berechnung der Distanz + Lenkerbreite (im handleEvent())

Original

// event is distance event
if (event.hasDistanceMeasurement() && event.distanceMeasurement.distance < 5) {
    // convert distance to cm + handlebar width
    val distance = ((event.distanceMeasurement.distance * 100) +
        SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidth(this)).toInt()
    // left sensor event
    if (event.distanceMeasurement.sourceId == 1) {
        binding.leftSensorTextView.text = this@OBSLiteActivity.getString(
            R.string.obs_lite_text_last_distance_left,
            distance
        )
        setColePassBarColor(distance, binding.leftSensorProgressBar)
        val eventTime = event.getTime(0).seconds
        if (startTime == -1L) {
            startTime = eventTime
        }
        // calculate minimal moving median for when the user presses obs lite button
        movingMedian.newValue(distance)
        // right sensor event
    } else {
        binding.rightSensorTextView.text = this@OBSLiteActivity.getString(
            R.string.obs_lite_text_last_distance_right,
            distance
        )
        setColePassBarColor(distance, binding.rightSensorProgressBar)
    }
    // event is user input event
} else if (event.hasUserInput()) {
    ...
}

Geänderte Version

// event is distance event
if (event.hasDistanceMeasurement() && event.distanceMeasurement.distance < 5) {
    // Rohwert (Meter -> cm)
    val rawDistanceCm = (event.distanceMeasurement.distance * 100).toInt()
    // left sensor event
    if (event.distanceMeasurement.sourceId == 1) {
        // Linker Sensor -> linke Lenkerbreite verwenden
        val handlebarWidthLeftCm =
            SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthLeft(this)
        val correctedDistanceLeft =
            (rawDistanceCm - handlebarWidthLeftCm).coerceAtLeast(0)

        binding.leftSensorTextView.text = this@OBSLiteActivity.getString(
            R.string.obs_lite_text_last_distance_left,
            correctedDistanceLeft
        )
        setColePassBarColor(correctedDistanceLeft, binding.leftSensorProgressBar)
        val eventTime = event.getTime(0).seconds
        if (startTime == -1L) {
            startTime = eventTime
        }
        // calculate minimal moving median for when the user presses obs lite button
        // Moving-Median arbeitet mit bereits korrigiertem Abstand (cm)
        movingMedian.newValue(correctedDistanceLeft)
        // right sensor event
    } else {
        // Rechter Sensor -> rechte Lenkerbreite verwenden
        val handlebarWidthRightCm =
            SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthRight(this)
        val correctedDistanceRight =
            (rawDistanceCm - handlebarWidthRightCm).coerceAtLeast(0)

        binding.rightSensorTextView.text = this@OBSLiteActivity.getString(
            R.string.obs_lite_text_last_distance_right,
            correctedDistanceRight
        )
        setColePassBarColor(correctedDistanceRight, binding.rightSensorProgressBar)
    }
    // event is user input event
} else if (event.hasUserInput()) {
    ...
}

b) Event beim User-Input (Button) (ebenfalls in handleEvent())

Original

// event is user input event
} else if (event.hasUserInput()) {
    val dm: DistanceMeasurement = DistanceMeasurement.newBuilder()
        .setDistance(movingMedian.median.toFloat()).build()

    event = event.toBuilder().setDistanceMeasurement(dm).build()

    binding.userInputProgressbarTextView.text =
        this@OBSLiteActivity.getString(
            R.string.overtake_distance_left,
            movingMedian.median
        )
    setColePassBarColor(
        movingMedian.median,
        binding.leftSensorUserInputProgressBar
    )
    binding.userInputTextView.text =
        this@OBSLiteActivity.getString(R.string.overtake_press_button) + event

}

Geänderte Version

// event is user input event
} else if (event.hasUserInput()) {
    // Der Median enthält bereits den korrigierten Abstand in cm
    val dm: DistanceMeasurement = DistanceMeasurement.newBuilder()
        .setDistance(movingMedian.median.toFloat()) // cm
        .build()

    event = event.toBuilder().setDistanceMeasurement(dm).build()

    binding.userInputProgressbarTextView.text =
        this@OBSLiteActivity.getString(
            R.string.overtake_distance_left,
            movingMedian.median
        )
    setColePassBarColor(
        movingMedian.median,
        binding.leftSensorUserInputProgressBar
    )
    binding.userInputTextView.text =
        this@OBSLiteActivity.getString(R.string.overtake_press_button) + event

}

RecorderService.java

1. Kommentar im InsertHandler

Original

        @SuppressLint("MissingPermission")
        public void run() {
            /*
              How is this Working?
              We are collecting GPS, Accelerometer, Gyroscope (and OpenBikeSensor) Data, those are updated as following;
              - GPS (lat, lon, accuracy) roughly every 3 seconds
              - accelerometer data (x,y,z) roughly 50 times a second
              - gyroscope data (a,b,c) roughly every 3 seconds
              <p>
              Every Data Type is given asynchronously via its Callback function.
              In order to synchronize the accelerometer interval ist used as baseline.
              1. We wait till there are 30 values generated
              2. We write a Log Entry every {@link Constants.MVG_AVG_STEP}
                 as this number of values is removed at the end of this function
                 and we wait again till there are 30
             */

Angepasst

        @SuppressLint("MissingPermission")
        public void run() {
            /*
              How is this Working?
              We are collecting GPS, Accelerometer, Gyroscope (and OpenBikeSensor) Data, those are updated as following;
              - GPS (lat, lon, accuracy) roughly every 3 seconds
              - accelerometer data (x,y,z) roughly 50 times a second
              - gyroscope data (a,b,c) roughly every 3 seconds

              Every Data Type is given asynchronously via its Callback function.
              In order to synchronize the accelerometer interval the accelerometer interval is used as baseline.
             */

Änderungen:

  • Tippfehler korrigiert: istthe accelerometer interval.
  • Der alte 1./2.-Erklärungsteil entfernt (Struktur vereinfacht).

2. OBS-Lite-Distanzlogik im InsertHandler

Original

                    if (obsLiteEvent != null && lastLocation != null) {
                        double handleBarLength = SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidth(RecorderService.this);
                        double eventDistance = obsLiteEvent.getDistanceMeasurement().getDistance() * 100.0;
                        double realDistance = handleBarLength + eventDistance;
                        if (realDistance >= 150) {
                            Log.d(TAG, "Adding hidden Close Pass with TS: " + lastAccUpdate + " realLeftDistance: " + realDistance);
                            incidentLog.updateOrAddIncident(IncidentLogEntry.newBuilder().withBaseInformation(lastAccUpdate,lastLocation.getLatitude(),lastLocation.getLongitude()).withIncidentType(IncidentLogEntry.INCIDENT_TYPE.OBS_LITE).withDescription(getString(R.string.overtake_distance_left,((int)obsLiteEvent.getDistanceMeasurement().getDistance()))).withKey(5000).build());
                        } else {
                            Log.d(TAG, "Adding visible Close Pass with TS: " + lastAccUpdate + " realLeftDistance: " + realDistance);
                            incidentLog.updateOrAddIncident(IncidentLogEntry.newBuilder().withBaseInformation(lastAccUpdate,lastLocation.getLatitude(),lastLocation.getLongitude()).withIncidentType(IncidentLogEntry.INCIDENT_TYPE.CLOSE_PASS).withDescription(getString(R.string.overtake_distance_left,obsLiteEvent.getDistanceMeasurement().getDistance())).withKey(4000).build());
                        }
                        obsLiteEvent = null;
                    }

Angepasst

                    // OBS-Lite Ereignisse (Knopf) – Distanz ist bereits korrigiert (cm) in DistanceMeasurement.distance
                    if (obsLiteEvent != null && lastLocation != null) {
                        double realDistanceCm = obsLiteEvent.getDistanceMeasurement().getDistance(); // bereits in cm
                        if (realDistanceCm < 0) {
                            realDistanceCm = 0;
                        }
                        int realDistanceInt = (int) Math.round(realDistanceCm);

                        if (realDistanceCm >= 150) {
                            Log.d(TAG, "Adding hidden Close Pass with TS: " + lastAccUpdate + " realLeftDistance(cm): " + realDistanceCm);
                            incidentLog.updateOrAddIncident(
                                    IncidentLogEntry.newBuilder()
                                            .withBaseInformation(lastAccUpdate, lastLocation.getLatitude(), lastLocation.getLongitude())
                                            .withIncidentType(IncidentLogEntry.INCIDENT_TYPE.OBS_LITE)
                                            .withDescription(getString(R.string.overtake_distance_left, realDistanceInt))
                                            .withKey(5000)
                                            .build()
                            );
                        } else {
                            Log.d(TAG, "Adding visible Close Pass with TS: " + lastAccUpdate + " realLeftDistance(cm): " + realDistanceCm);
                            incidentLog.updateOrAddIncident(
                                    IncidentLogEntry.newBuilder()
                                            .withBaseInformation(lastAccUpdate, lastLocation.getLatitude(), lastLocation.getLongitude())
                                            .withIncidentType(IncidentLogEntry.INCIDENT_TYPE.CLOSE_PASS)
                                            .withDescription(getString(R.string.overtake_distance_left, realDistanceInt))
                                            .withKey(4000)
                                            .build()
                            );
                        }
                        obsLiteEvent = null;
                    }

Änderungen im Verhalten:

  • getDistance() wird jetzt als bereits korrigierte Distanz in cm interpretiert:
    • Kein * 100.0 mehr.
    • Keine Addition von Lenkerbreite + 13 cm mehr.
  • Negative Werte werden auf 0 gekappt.
  • Beschreibungstext nutzt den aufgerundeten Integer-Wert realDistanceInt.
  • Logs sind explizit mit „(cm)“ gekennzeichnet.

Update:
Hallo zusammen

Kann das wer verifizieren? Denn ich kann es nicht überprüfen, was das Sprachmodell gefunden zu haben meint.

Und zwar in OBSLiteSession.kt: Code auf Github.

Die Rohdistanzen landen im Binary, aber ohne Lenkerbreiten-Korrektur. Und dieses obsLiteSessionXY.bin wird dann ins portal geladen. Stimmt das? Und was wurde nicht berücksichtigt?

DistanceMeasurement wird unverändert ins Event übernommen

Im handleEvent:


if (obsEvent.hasDistanceMeasurement()) {
    val dm = obsEvent.distanceMeasurement
    obsEvent = obsEvent.toBuilder()
        .addTime(obsTime)
        .addTime(smartphoneTime)
        .setDistanceMeasurement(dm)
        .build()
    // left sensor event
    if (obsEvent.distanceMeasurement.sourceId == 1) {
        val distance = (
            (obsEvent.distanceMeasurement.distance * 100)
            + SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthLeft(context)
        ).toInt()
        // calculate minimal moving median for when the user presses obs lite button
        movingMedian.newValue(distance)
    }
}
  • val dm = obsEvent.distanceMeasurement → dm ist der Rohwert vom OBS-Lite-Board (Sensor → Objekt, in m).
  • setDistanceMeasurement(dm) → genau dieses dm wird wieder in obsEvent gesetzt, ohne Veränderung von dm.distance.
  • Die Rechnung mit Lenkerbreite passiert nur in der lokalen Variable distance (in cm) für movingMedian – dm bleibt unangetastet

ich habe das anpassen lassen und dann testfahrten gemacht.
und zusätzlich den sensor nicht mehr an der sattelstütze befestigt, sondern am rack des Lastenvelos. Dadruch wurden die 0.23-0.25m-Messungen eliminiert. Es waren wohl meine Beine, die zu diesen Messungen geführt haben.

package de.tuberlin.mcc.simra.app.obslite

import android.content.Context
import android.location.Location
import android.util.Log
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import de.tuberlin.mcc.simra.app.BuildConfig
import de.tuberlin.mcc.simra.app.DistanceMeasurement
import de.tuberlin.mcc.simra.app.Event
import de.tuberlin.mcc.simra.app.Geolocation
import de.tuberlin.mcc.simra.app.Metadata
import de.tuberlin.mcc.simra.app.Time
import de.tuberlin.mcc.simra.app.activities.OBSLiteActivity
import de.tuberlin.mcc.simra.app.util.CobsUtils
import de.tuberlin.mcc.simra.app.util.SharedPref
import java.util.LinkedList
import java.util.concurrent.ConcurrentLinkedDeque

class OBSLiteSession(val context: Context) {
    var obsLiteStartTime: Long = 0L
    val TAG = "OBSLiteSession_LOG"
    private var events: ArrayList<Event> = ArrayList()
    var byteListQueue = ConcurrentLinkedDeque<LinkedList<Byte>>()
    var lastByteRead: Byte? = null
    var movingMedian: OBSLiteActivity.MovingMedian = OBSLiteActivity.MovingMedian()
    var startTime = -1L
    private var lastLat: Double = 0.0
    private var lastLon: Double = 0.0
    private var completeEvents = ArrayList<Byte>()

    init {

        val handlebarOffsetLeft: ByteString = ByteString.copyFromUtf8(
            SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthLeft(context).toString()
        )
        val handlebarOffsetRight: ByteString = ByteString.copyFromUtf8(
            SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthRight(context).toString()
        )
        val metaData: Metadata = Metadata.newBuilder()
            .putData("HandlebarOffsetLeft", handlebarOffsetLeft)
            .putData("HandlebarOffsetRight", handlebarOffsetRight)
            .putData("SimRaVersion", ByteString.copyFromUtf8(BuildConfig.VERSION_NAME))
            .build()
        // Nur intern merken, NICHT in completeEvents (Binary), sonst hat das Portal ein Event ohne Zeit
        events.add(Event.newBuilder().setMetadata(metaData).build())
    }

    // handles distance event and user input events of obs lite
    fun handleEvent(lat: Double, lon: Double, altitude: Double, accuracy: Float): Event? {
        val decodedData = CobsUtils.decode(byteListQueue.first)

        try {
            var obsEvent: Event = Event.parseFrom(decodedData)
            val currentTimeMillis: Long = System.currentTimeMillis()

            if (startTime == -1L) {
                startTime = obsEvent.getTime(0).seconds
            }

            val obsTime: Time = Time.newBuilder()
                .setSeconds(obsEvent.getTime(0).seconds)
                .setNanoseconds(obsEvent.getTime(0).nanoseconds)
                .setSourceId(2).setReference(Time.Reference.UNIX).build()

            val smartphoneTime: Time = Time.newBuilder()
                .setSeconds(currentTimeMillis / 1000)
                .setNanoseconds(((currentTimeMillis % 1000) * 1000000).toInt())
                .setSourceId(3).setReference(Time.Reference.UNIX).build()

            if (lat != lastLat || lon != lastLon) {
                val geolocation: Geolocation = Geolocation.newBuilder()
                    .setLatitude(lat).setLongitude(lon)
                    .setAltitude(altitude).setHdop(accuracy).build()

                val gpsEvent = Event.newBuilder()
                    .setGeolocation(geolocation)
                    .addTime(obsTime)
                    .addTime(smartphoneTime)
                    .build()
                events.add(gpsEvent)
                completeEvents.addAll(encodeEvent(gpsEvent))
                lastLat = lat
                lastLon = lon
            }

            if (obsEvent.hasDistanceMeasurement()) {
                // >>> Änderung: Lenkerbreite vom linken Sensor-Abstand abziehen
                var dm: DistanceMeasurement = obsEvent.distanceMeasurement
                var dmBuilder = dm.toBuilder()

                if (dm.sourceId == 1) {
                    val handlebarLeftCm =
                        SharedPref.Settings.Ride.OvertakeWidth.getHandlebarWidthLeft(context)

                    val rawMeters = dm.distance                 // Sensor -> Objekt
                    val handlebarMeters = handlebarLeftCm / 100.0f

                    var correctedMeters = rawMeters - handlebarMeters
                    if (correctedMeters < 0f) {
                        correctedMeters = 0f
                    }

                    dmBuilder = dmBuilder.setDistance(correctedMeters)

                    val correctedCm = (correctedMeters * 100.0f)
                        .toInt()
                        .coerceAtLeast(0)
                    // calculate minimal moving median for when the user presses obs lite button
                    movingMedian.newValue(correctedCm)
                } else {
                    // andere Sensoren unverändert lassen
                }

                dm = dmBuilder.build()
                obsEvent = obsEvent.toBuilder()
                    .addTime(obsTime)
                    .addTime(smartphoneTime)
                    .setDistanceMeasurement(dm)
                    .build()

            } else if (obsEvent.hasUserInput()) {
                val ui = obsEvent.userInput
                obsEvent = obsEvent.toBuilder()
                    .addTime(obsTime)
                    .addTime(smartphoneTime)
                    .setUserInput(ui)
                    .build()
                events.add(obsEvent)
                completeEvents.addAll(encodeEvent(obsEvent))
                val dm: DistanceMeasurement = DistanceMeasurement.newBuilder()
                    .setDistance(movingMedian.median.toFloat()).build()
                obsEvent = obsEvent.toBuilder()
                    .addTime(obsTime)
                    .addTime(smartphoneTime)
                    .setDistanceMeasurement(dm).build()
                // Log.d(TAG, "user input event: $obsEvent")
                byteListQueue.removeFirst()
                return obsEvent

            } else {
                obsEvent = obsEvent.toBuilder()
                    .addTime(obsTime)
                    .addTime(smartphoneTime)
                    .build()
                // Log.d(TAG, obsEvent.toString())
            }

            // Log.d(TAG, obsEvent.toString())
            events.add(obsEvent)
            completeEvents.addAll(encodeEvent(obsEvent))

        } catch (_: InvalidProtocolBufferException) {
        }
        // if first byte list is handled, remove it.
        byteListQueue.removeFirst()
        return null
    }

    // checks whether the next byteList in queue is a complete COBS package
    fun completeCobsAvailable(): Boolean {
        for (aByte in byteListQueue.peekFirst()!!) {
            if (aByte.toInt() == 0x00) {
                return true
            }
        }
        return false
    }

    // handles the byteListQueues, which contain the COBS packages
    fun fillByteList(data: ByteArray?) {
        for (datum in data!!) {
            // start new COBS package when last byte was 00 or it is the first data
            if (lastByteRead?.toInt() == 0x00 || byteListQueue.isEmpty()) {
                val newByteList = LinkedList<Byte>()
                newByteList.add(datum)
                byteListQueue.add(newByteList)
            } else { // COBS package is not completed yet, continue the same package
                byteListQueue.last.add(datum)
            }
            lastByteRead = datum
        }
    }

    // pretty print the byteListQueue
    fun printByteList() {
        val byteListQueueSB = StringBuilder()
        byteListQueueSB.append("[")
        for (byteList in byteListQueue) {
            byteListQueueSB.append("[")
            val sb = StringBuilder()
            for (byte in byteList) {
                sb.append(String.format("%02x", byte))
            }
            byteListQueueSB.append(sb.toString()).append("], ")
        }
        byteListQueueSB.append("]")
        Log.d(TAG, "byteListQueueSB: $byteListQueueSB")
    }

    private fun encodeEvent(event: Event?): Collection<Byte> {
        // Log.d(TAG, event.toString())
        val byteArray = event?.toByteArray()
        return CobsUtils.encode2(byteArray)
        /*Log.d(TAG, "=========================")
        Log.d(TAG, Event.parseFrom(CobsUtils.decode(cobsByteArray)).toString())*/
    }

    fun getCompleteEvents(): ByteArray {
        return completeEvents.toByteArray()
    }

    // Adds a GPS event with the new Lat,Lon and the time of the new GPS
    // (used by RecorderService's onLocationChanged)
    fun addGPSEvent(location: Location) {
        val geolocation: Geolocation = Geolocation.newBuilder()
            .setLatitude(location.latitude).setLongitude(location.longitude)
            .setAltitude(location.altitude).setHdop(location.accuracy).build()

        val time: Time = Time.newBuilder()
            .setNanoseconds((location.time * 1000000).toInt()).build()

        val gpsEvent = Event.newBuilder().setGeolocation(geolocation).addTime(time).build()
        events.add(gpsEvent)
        completeEvents.addAll(encodeEvent(gpsEvent))
    }
}