From 0403bbb270d73f2cfe455a1f4e1fff0de41467a2 Mon Sep 17 00:00:00 2001 From: Severin Kaderli Date: Wed, 7 Dec 2022 23:32:44 +0100 Subject: [PATCH] Add more or less working prototype Signed-off-by: Severin Kaderli --- .../java/dev/kaderli/magsend/BaseActivity.kt | 3 + .../dev/kaderli/magsend/BaseSensorActivity.kt | 14 +- .../dev/kaderli/magsend/ReceiveActivity.kt | 232 +++++++++++++++++- .../java/dev/kaderli/magsend/model/Sample.kt | 13 + .../java/dev/kaderli/magsend/model/Signal.kt | 9 + .../src/main/res/layout/activity_receive.xml | 29 ++- .../app/src/main/res/values/colors.xml | 4 +- .../app/src/main/res/values/dimens.xml | 1 + .../app/src/main/res/values/strings.xml | 3 + src/website/scripts/Constants.js | 11 +- src/website/scripts/Main.js | 17 +- src/website/scripts/Utility.js | 2 +- 12 files changed, 321 insertions(+), 17 deletions(-) create mode 100644 src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Sample.kt create mode 100644 src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Signal.kt diff --git a/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseActivity.kt b/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseActivity.kt index e32fff1..97f2ec0 100644 --- a/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseActivity.kt +++ b/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseActivity.kt @@ -6,6 +6,9 @@ import androidx.appcompat.widget.Toolbar abstract class BaseActivity : AppCompatActivity() { + @Suppress("PropertyName") + protected val TAG: String = this::class.java.name + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(getLayoutId()) diff --git a/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseSensorActivity.kt b/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseSensorActivity.kt index be9db7b..aefa402 100644 --- a/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseSensorActivity.kt +++ b/src/MagSend/app/src/main/java/dev/kaderli/magsend/BaseSensorActivity.kt @@ -9,15 +9,23 @@ import android.os.Bundle import kotlin.math.pow import kotlin.math.sqrt +const val SAMPLING_RATE = 0 + + abstract class BaseSensorActivity : BaseActivity(), SensorEventListener { private lateinit var sensorManager: SensorManager; private lateinit var sensor: Sensor; - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onResume() { + super.onResume() sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager sensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI); + sensorManager.registerListener(this, sensor, SAMPLING_RATE); + } + + override fun onStop() { + super.onStop() + sensorManager.unregisterListener(this, sensor) } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} diff --git a/src/MagSend/app/src/main/java/dev/kaderli/magsend/ReceiveActivity.kt b/src/MagSend/app/src/main/java/dev/kaderli/magsend/ReceiveActivity.kt index 6057d00..c01c3b3 100644 --- a/src/MagSend/app/src/main/java/dev/kaderli/magsend/ReceiveActivity.kt +++ b/src/MagSend/app/src/main/java/dev/kaderli/magsend/ReceiveActivity.kt @@ -1,7 +1,237 @@ package dev.kaderli.magsend -class ReceiveActivity : BaseActivity() { +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.core.content.ContextCompat +import dev.kaderli.magsend.model.Sample +import dev.kaderli.magsend.model.Signal +import java.lang.Integer.min + +const val NEEDED_SAMPLES_PER_SECOND = 190 + +class ReceiveActivity : BaseSensorActivity() { + private lateinit var preambleStatus: TextView + private lateinit var headerStatus: TextView + private lateinit var receiveValue: TextView + + /** + * List that contains all received samples from the sensor. + */ + private var samples = ArrayList>() + + /** + * A list that contains the samples as high or low signals. + */ + private var signal = ArrayList>() + + private var preambleReceived = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + preambleStatus = findViewById(R.id.preambleStatus) + headerStatus = findViewById(R.id.headerStatus) + receiveValue = findViewById(R.id.receiveValue) + } + override fun getLayoutId(): Int { return R.layout.activity_receive } + + private fun getRangeOfData(): Float { + val maxValue = samples.maxByOrNull { it.value } ?: Sample(0f) + val minValue = samples.minByOrNull { it.value } ?: Sample(0f) + //Log.i(TAG, "Min: ${minValue}, Max: $maxValue") + return maxValue.value - minValue.value + } + + private fun getThresholdForHighSignals(): Float { + if (getRangeOfData() < 3) { + return getMidRangeOfData() + 3 + } + + return getMidRangeOfData(); + } + + private fun getMidRangeOfData(): Float { + val maxValue = samples.maxByOrNull { it.value } ?: Sample(0f) + val minValue = samples.minByOrNull { it.value } ?: Sample(0f) + return (maxValue.value + minValue.value) / 2; + } + + private fun isHigh(value: Float): Boolean { + return value > getThresholdForHighSignals() + } + + private fun detectPreamble(): Boolean { + val cleanedSignal = cleanSignal(signal) + val compressedSignal = compressSignal(cleanedSignal) + + if (compressedSignal.size < 4) { + return false + } + + for (i in 0..compressedSignal.size - 4) { + val firstValue = compressedSignal[i] + val secondValue = compressedSignal[i + 1] + val thirdValue = compressedSignal[i + 2] + val fourthValue = compressedSignal[i + 3] + + if (firstValue.value != Signal.High || secondValue.value != Signal.Low || thirdValue.value != Signal.High) { + continue + } + + if (secondValue.timestamp - firstValue.timestamp > 2900 && thirdValue.timestamp - secondValue.timestamp > 2900 && fourthValue.timestamp - thirdValue.timestamp > 2900) { + return true + } + } + + return false + } + + private fun cleanSignal(signal: List>): List> { + // Remove outliers from the signal (single signal spikes or drops) + val cleanSignal = signal.filterIndexed { index, sample -> + var elementCount = 1 + + var previousIndex = index - 1 + while (previousIndex > 0 && signal[previousIndex].value == sample.value) { + elementCount++ + previousIndex-- + } + + var nextIndex = index + 1 + while (nextIndex < signal.size && signal[nextIndex].value == sample.value) { + elementCount++ + nextIndex++ + } + + elementCount > 5 + } + + return cleanSignal + } + + private fun compressSignal(signal: List>): List> { + // Only keep the first of consecutive same signals + val compactSignal = ArrayList>() + signal.forEach { sample -> + if (sample.value != compactSignal.lastOrNull()?.value) { + compactSignal.add(sample) + } + } + + return compactSignal + } + + private fun analyzePacketSignal(): List { + val cleanedSignal = cleanSignal(signal) + val compressedSignal = compressSignal(cleanedSignal) + + if (compressedSignal.size < 2) { + return listOf() + } + + val timeDifferences = ArrayList() + compressedSignal.forEachIndexed { index, sample -> + if (index == 0) { + return@forEachIndexed + } + + timeDifferences.add(sample.timestamp - compressedSignal[index - 1].timestamp) + } + + val clockTimeThreshold = 460//timeDifferences.max() * 0.75 + + val packetData = ArrayList() + compressedSignal.forEachIndexed { index, sample -> + if (index == 0) { + return@forEachIndexed + } + + val timeDifference = sample.timestamp - compressedSignal[index - 1].timestamp + if (timeDifference > clockTimeThreshold && timeDifference < 2500) { + if (sample.value == Signal.High) { + packetData.add(0) + } else { + packetData.add(1) + } + } + } + + //Log.i(TAG, "$timeDifferences") + //Log.i(TAG, "$cleanSignal") + + return packetData + } + + override fun sensorValueReceived(magneticFieldStrength: Float) { + // Add the current received sensor value to the samples + samples.add(Sample(magneticFieldStrength)) + + // Determine whether the sample is a high or low signal + val currentSignal: Sample = if (isHigh(magneticFieldStrength)) { + Sample(Signal.High) + } else { + Sample(Signal.Low) + } + signal.add(currentSignal) + + // Try to detect the preamble in the signal + if (!preambleReceived) { + if (detectPreamble()) { + preambleReceived = true + preambleStatus.setText(R.string.preamble_status_detected) + preambleStatus.setTextColor(ContextCompat.getColor(this, R.color.success)) + signal.clear() + return + } + } + + if (preambleReceived) { + val packet = analyzePacketSignal(); + + var payloadLength = 0 + if (packet.size >= 4) { + payloadLength = listToInteger(packet.take(4)) + headerStatus.text = getString(R.string.payload_length, payloadLength) + } + + if (packet.size >= 4 + (8)) { + val numberOfAvailablePayloadBytes = min((packet.size - 4) / 8, payloadLength) + var payload = "" + for (i in 1..numberOfAvailablePayloadBytes) { + payload += listToInteger(packet.take(4 + (8 * i)).takeLast(8)).toChar().toString() + } + + receiveValue.text = payload + + if (packet.size >= 4 + (8 * payloadLength) + 8) { + // CRC Check + val crc = listToInteger(packet.take(4 + (8 * payloadLength) + 8).takeLast(8)) + Log.i(TAG, "CRC: $crc") + } + } + } + } + + private fun listToInteger(values: List): Int { + var value: Int = 0 + + for (bit in values) { + value = (value shl 1) + bit + } + + return value + } + + + fun restartReceiveProcess(view: View) { + preambleReceived = false + preambleStatus.setText(R.string.preamble_status_not_detected) + preambleStatus.setTextColor(ContextCompat.getColor(this, R.color.error)) + samples.clear() + signal.clear() + } } diff --git a/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Sample.kt b/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Sample.kt new file mode 100644 index 0000000..89057d5 --- /dev/null +++ b/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Sample.kt @@ -0,0 +1,13 @@ +package dev.kaderli.magsend.model + +/** + * A simple data class that takes a value and stores it together with the + * current timestamp. + */ +data class Sample(val value: T) { + val timestamp: Long = System.currentTimeMillis() + + override fun toString(): String { + return value.toString() + } +} diff --git a/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Signal.kt b/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Signal.kt new file mode 100644 index 0000000..465e71a --- /dev/null +++ b/src/MagSend/app/src/main/java/dev/kaderli/magsend/model/Signal.kt @@ -0,0 +1,9 @@ +package dev.kaderli.magsend.model + +/** + * A simple ENUM that is either low or high. + */ +enum class Signal { + Low, + High, +} diff --git a/src/MagSend/app/src/main/res/layout/activity_receive.xml b/src/MagSend/app/src/main/res/layout/activity_receive.xml index cfe2140..ba3c5e3 100644 --- a/src/MagSend/app/src/main/res/layout/activity_receive.xml +++ b/src/MagSend/app/src/main/res/layout/activity_receive.xml @@ -12,27 +12,51 @@ android:id="@+id/receiveDescription" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingHorizontal="@dimen/layout_margins" android:text="@string/receive_description" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/toolbar" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/headerStatus" /> #FF018786 #FF000000 #FFFFFFFF - \ No newline at end of file + #FFFF0000 + #FF00FF00 + diff --git a/src/MagSend/app/src/main/res/values/dimens.xml b/src/MagSend/app/src/main/res/values/dimens.xml index caefc41..3cbe198 100644 --- a/src/MagSend/app/src/main/res/values/dimens.xml +++ b/src/MagSend/app/src/main/res/values/dimens.xml @@ -3,4 +3,5 @@ 16dp 24dp 48sp + 8dp diff --git a/src/MagSend/app/src/main/res/values/strings.xml b/src/MagSend/app/src/main/res/values/strings.xml index 067e3a0..1d4e639 100644 --- a/src/MagSend/app/src/main/res/values/strings.xml +++ b/src/MagSend/app/src/main/res/values/strings.xml @@ -11,4 +11,7 @@ Calibration Receive %1$.2f µT + Preamble not detected X + Preamble detected ✓ + The payload length is %1$d bytes diff --git a/src/website/scripts/Constants.js b/src/website/scripts/Constants.js index 3987b6d..9b1cd94 100644 --- a/src/website/scripts/Constants.js +++ b/src/website/scripts/Constants.js @@ -1,10 +1,19 @@ class Constants { /** - * The amount of time in ms a bit takes to transmit. + * The amount of time in ms a symbol takes to transmit. * * @type {Number} */ static get CLOCK_TIME() { + return 500; + } + + /** + * The amount of time in ms a symbol of the preamble takes to transmit. + * + * @type {Number} + */ + static get PREAMBLE_CLOCK_TIME() { return 1000; } diff --git a/src/website/scripts/Main.js b/src/website/scripts/Main.js index 4b1bfb8..9b2ccf5 100644 --- a/src/website/scripts/Main.js +++ b/src/website/scripts/Main.js @@ -81,7 +81,7 @@ async function startCalibration() { return; } - await transmitBit(Constants.CALIBRATION_SIGNAL[i]); + await transmitBit(Constants.CALIBRATION_SIGNAL[i], Constants.CLOCK_TIME); } } } @@ -124,6 +124,7 @@ function startSending() { stopSendingButton.classList.remove(Constants.HIDE_CLASS); const packet = new Packet(textInput.value); + console.log(packet); const signal = Utility.manchesterEncode(packet.getData()); isTransmitting = true; @@ -158,7 +159,7 @@ async function transmitSignal(signal) { } console.log(`Sending preamble: ${bit}`); - await transmitBit(bit); + await transmitBit(bit, Constants.PREAMBLE_CLOCK_TIME); } for (let i = 0; i < signal.length; i++) { @@ -167,24 +168,24 @@ async function transmitSignal(signal) { return; } - console.log(`Sending bit ${i + 1} of ${signal.length} of packet: ${signal[i]}`); - await transmitBit(signal[i]); + console.log(`Sending symbol ${i + 1} of ${signal.length} of packet: ${signal[i]}`); + await transmitBit(signal[i], Constants.CLOCK_TIME); } } } /** * Starts or stops the web workers depending on the bit value and then waits - * for a duration of Constants.CLOCK_TIME. + * for a duration of clockTime. * * @param {Number} bit */ -function transmitBit(bit) { +function transmitBit(bit, clockTime) { if (bit === 1) { startWorkers(); - return new Promise((resolve) => setTimeout(resolve, Constants.CLOCK_TIME)); + return new Promise((resolve) => setTimeout(resolve, clockTime)); } stopWorkers(); - return new Promise((resolve) => setTimeout(resolve, Constants.CLOCK_TIME)); + return new Promise((resolve) => setTimeout(resolve, clockTime)); } diff --git a/src/website/scripts/Utility.js b/src/website/scripts/Utility.js index cb41225..a76aa0f 100644 --- a/src/website/scripts/Utility.js +++ b/src/website/scripts/Utility.js @@ -56,8 +56,8 @@ class Utility { const encodedBits = []; for (const bit of bitArray) { - encodedBits.push(bit ^ 1); encodedBits.push(bit ^ 0); + encodedBits.push(bit ^ 1); } return encodedBits;