Add more or less working prototype
Signed-off-by: Severin Kaderli <severin@kaderli.dev>
This commit is contained in:
parent
99d93c573d
commit
0403bbb270
12 changed files with 321 additions and 17 deletions
|
@ -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())
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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<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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.kaderli.magsend.model
|
||||
|
||||
/**
|
||||
* A simple ENUM that is either low or high.
|
||||
*/
|
||||
enum class Signal {
|
||||
Low,
|
||||
High,
|
||||
}
|
|
@ -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" />
|
||||
|
||||
<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
|
||||
android:id="@+id/receiveValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/component_spacing"
|
||||
android:text="82µT "
|
||||
android:text="___"
|
||||
android:textSize="48sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/receiveDescription" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/headerStatus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/restartDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/component_spacing"
|
||||
android:paddingHorizontal="@dimen/layout_margins"
|
||||
android:text="@string/restart_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -43,6 +67,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:onClick="restartReceiveProcess"
|
||||
android:text="@string/restart_button_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -7,4 +7,6 @@
|
|||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="error">#FFFF0000</color>
|
||||
<color name="success">#FF00FF00</color>
|
||||
</resources>
|
|
@ -3,4 +3,5 @@
|
|||
<dimen name="layout_margins">16dp</dimen>
|
||||
<dimen name="component_spacing">24dp</dimen>
|
||||
<dimen name="title_text_size">48sp</dimen>
|
||||
<dimen name="component_spacing_small">8dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -11,4 +11,7 @@
|
|||
<string name="calibration_activity_label">Calibration</string>
|
||||
<string name="receive_activity_label">Receive</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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Reference in a new issue