Add more or less working prototype

Signed-off-by: Severin Kaderli <severin@kaderli.dev>
This commit is contained in:
Severin Kaderli 2022-12-07 23:32:44 +01:00
parent 99d93c573d
commit 0403bbb270
Signed by: severinkaderli
GPG key ID: F419F8835B72F0C4
12 changed files with 321 additions and 17 deletions

View file

@ -6,6 +6,9 @@ import androidx.appcompat.widget.Toolbar
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
@Suppress("PropertyName")
protected val TAG: String = this::class.java.name
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(getLayoutId()) setContentView(getLayoutId())

View file

@ -9,15 +9,23 @@ import android.os.Bundle
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
const val SAMPLING_RATE = 0
abstract class BaseSensorActivity : BaseActivity(), SensorEventListener { abstract class BaseSensorActivity : BaseActivity(), SensorEventListener {
private lateinit var sensorManager: SensorManager; private lateinit var sensorManager: SensorManager;
private lateinit var sensor: Sensor; private lateinit var sensor: Sensor;
override fun onCreate(savedInstanceState: Bundle?) { override fun onResume() {
super.onCreate(savedInstanceState) super.onResume()
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); 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) {} override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

View file

@ -1,7 +1,237 @@
package dev.kaderli.magsend 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<Sample<Float>>()
/**
* A list that contains the samples as high or low signals.
*/
private var signal = ArrayList<Sample<Signal>>()
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 { override fun getLayoutId(): Int {
return R.layout.activity_receive 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<Sample<Signal>>): List<Sample<Signal>> {
// 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<Sample<Signal>>): List<Sample<Signal>> {
// Only keep the first of consecutive same signals
val compactSignal = ArrayList<Sample<Signal>>()
signal.forEach { sample ->
if (sample.value != compactSignal.lastOrNull()?.value) {
compactSignal.add(sample)
}
}
return compactSignal
}
private fun analyzePacketSignal(): List<Int> {
val cleanedSignal = cleanSignal(signal)
val compressedSignal = compressSignal(cleanedSignal)
if (compressedSignal.size < 2) {
return listOf()
}
val timeDifferences = ArrayList<Long>()
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<Int>()
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<Signal> = 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>): 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()
}
} }

View file

@ -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<T>(val value: T) {
val timestamp: Long = System.currentTimeMillis()
override fun toString(): String {
return value.toString()
}
}

View file

@ -0,0 +1,9 @@
package dev.kaderli.magsend.model
/**
* A simple ENUM that is either low or high.
*/
enum class Signal {
Low,
High,
}

View file

@ -12,27 +12,51 @@
android:id="@+id/receiveDescription" android:id="@+id/receiveDescription"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/layout_margins"
android:text="@string/receive_description" android:text="@string/receive_description"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<TextView
android:id="@+id/preambleStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/component_spacing_small"
android:paddingHorizontal="@dimen/layout_margins"
android:text="@string/preamble_status_not_detected"
android:textColor="#FF0000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/receiveDescription" />
<TextView
android:id="@+id/headerStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/component_spacing_small"
android:paddingHorizontal="@dimen/layout_margins"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preambleStatus" />
<TextView <TextView
android:id="@+id/receiveValue" android:id="@+id/receiveValue"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/component_spacing" android:layout_marginTop="@dimen/component_spacing"
android:text="82µT " android:text="___"
android:textSize="48sp" android:textSize="48sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/receiveDescription" /> app:layout_constraintTop_toBottomOf="@+id/headerStatus" />
<TextView <TextView
android:id="@+id/restartDescription" android:id="@+id/restartDescription"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/component_spacing" android:layout_marginTop="@dimen/component_spacing"
android:paddingHorizontal="@dimen/layout_margins"
android:text="@string/restart_description" android:text="@string/restart_description"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -43,6 +67,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:onClick="restartReceiveProcess"
android:text="@string/restart_button_label" android:text="@string/restart_button_label"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -7,4 +7,6 @@
<color name="teal_700">#FF018786</color> <color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="error">#FFFF0000</color>
<color name="success">#FF00FF00</color>
</resources> </resources>

View file

@ -3,4 +3,5 @@
<dimen name="layout_margins">16dp</dimen> <dimen name="layout_margins">16dp</dimen>
<dimen name="component_spacing">24dp</dimen> <dimen name="component_spacing">24dp</dimen>
<dimen name="title_text_size">48sp</dimen> <dimen name="title_text_size">48sp</dimen>
<dimen name="component_spacing_small">8dp</dimen>
</resources> </resources>

View file

@ -11,4 +11,7 @@
<string name="calibration_activity_label">Calibration</string> <string name="calibration_activity_label">Calibration</string>
<string name="receive_activity_label">Receive</string> <string name="receive_activity_label">Receive</string>
<string name="calibration_value">%1$.2f µT</string> <string name="calibration_value">%1$.2f µT</string>
<string name="preamble_status_not_detected">Preamble not detected X</string>
<string name="preamble_status_detected">Preamble detected ✓</string>
<string name="payload_length">The payload length is %1$d bytes</string>
</resources> </resources>

View file

@ -1,10 +1,19 @@
class Constants { 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} * @type {Number}
*/ */
static get CLOCK_TIME() { 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; return 1000;
} }

View file

@ -81,7 +81,7 @@ async function startCalibration() {
return; 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); stopSendingButton.classList.remove(Constants.HIDE_CLASS);
const packet = new Packet(textInput.value); const packet = new Packet(textInput.value);
console.log(packet);
const signal = Utility.manchesterEncode(packet.getData()); const signal = Utility.manchesterEncode(packet.getData());
isTransmitting = true; isTransmitting = true;
@ -158,7 +159,7 @@ async function transmitSignal(signal) {
} }
console.log(`Sending preamble: ${bit}`); console.log(`Sending preamble: ${bit}`);
await transmitBit(bit); await transmitBit(bit, Constants.PREAMBLE_CLOCK_TIME);
} }
for (let i = 0; i < signal.length; i++) { for (let i = 0; i < signal.length; i++) {
@ -167,24 +168,24 @@ async function transmitSignal(signal) {
return; return;
} }
console.log(`Sending bit ${i + 1} of ${signal.length} of packet: ${signal[i]}`); console.log(`Sending symbol ${i + 1} of ${signal.length} of packet: ${signal[i]}`);
await transmitBit(signal[i]); await transmitBit(signal[i], Constants.CLOCK_TIME);
} }
} }
} }
/** /**
* Starts or stops the web workers depending on the bit value and then waits * 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 * @param {Number} bit
*/ */
function transmitBit(bit) { function transmitBit(bit, clockTime) {
if (bit === 1) { if (bit === 1) {
startWorkers(); startWorkers();
return new Promise((resolve) => setTimeout(resolve, Constants.CLOCK_TIME)); return new Promise((resolve) => setTimeout(resolve, clockTime));
} }
stopWorkers(); stopWorkers();
return new Promise((resolve) => setTimeout(resolve, Constants.CLOCK_TIME)); return new Promise((resolve) => setTimeout(resolve, clockTime));
} }

View file

@ -56,8 +56,8 @@ class Utility {
const encodedBits = []; const encodedBits = [];
for (const bit of bitArray) { for (const bit of bitArray) {
encodedBits.push(bit ^ 1);
encodedBits.push(bit ^ 0); encodedBits.push(bit ^ 0);
encodedBits.push(bit ^ 1);
} }
return encodedBits; return encodedBits;