diff --git a/base/api/jvm/base.api b/base/api/jvm/base.api index e75cca84f..304f3165e 100644 --- a/base/api/jvm/base.api +++ b/base/api/jvm/base.api @@ -69,9 +69,24 @@ public abstract interface class com/splendo/kaluga/base/bytes/ByteArrayBuilder { public abstract fun getByteOrder ()Lcom/splendo/kaluga/base/bytes/ByteOrder; } +public final class com/splendo/kaluga/base/bytes/ByteArrayBuilder$DefaultImpls { + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;CLcom/splendo/kaluga/base/bytes/Encoding;Lcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;DLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;FLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;ILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;JLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;Ljava/lang/String;Lcom/splendo/kaluga/base/bytes/StringEncodingSettings;Lcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;SLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add-4PLdz1A$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;JLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add-593hS54$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;ILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add-QCWLtZ4$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;ILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add-qim9Vi0$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;ILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V + public static synthetic fun add-vckuEUM$default (Lcom/splendo/kaluga/base/bytes/ByteArrayBuilder;SLcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)V +} + public final class com/splendo/kaluga/base/bytes/ByteArrayBuilderKt { - public static final fun buildByteArray (Lcom/splendo/kaluga/base/bytes/ByteOrder;Lkotlin/jvm/functions/Function1;)[B - public static synthetic fun buildByteArray$default (Lcom/splendo/kaluga/base/bytes/ByteOrder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)[B + public static final fun buildByteArray (Lcom/splendo/kaluga/base/bytes/ByteOrder;ILkotlin/jvm/functions/Function1;)[B + public static synthetic fun buildByteArray$default (Lcom/splendo/kaluga/base/bytes/ByteOrder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)[B } public final class com/splendo/kaluga/base/bytes/ByteExtensionsKt { @@ -110,6 +125,10 @@ public final class com/splendo/kaluga/base/bytes/CRC$Companion { public static synthetic fun invoke-1WdNxsU$default (Lcom/splendo/kaluga/base/bytes/CRC$Companion;IJJJZZILjava/lang/Object;)Lcom/splendo/kaluga/base/bytes/CRC; } +public final class com/splendo/kaluga/base/bytes/CRC$DefaultImpls { + public static fun getByteWidth (Lcom/splendo/kaluga/base/bytes/CRC;)I +} + public final class com/splendo/kaluga/base/bytes/CRC10 : com/splendo/kaluga/base/bytes/CRC { public static final field INSTANCE Lcom/splendo/kaluga/base/bytes/CRC10; public fun compute-I7RO_PI ([B)J @@ -961,18 +980,24 @@ public final class com/splendo/kaluga/base/bytes/CRCB : com/splendo/kaluga/base/ } public final class com/splendo/kaluga/base/bytes/CharExtensionsKt { + public static final fun copyCharIntoByteArray (Lcom/splendo/kaluga/base/bytes/Encoding;C[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyCharIntoByteArray$default (Lcom/splendo/kaluga/base/bytes/Encoding;C[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B + public static final fun copyUTF16IntoByteArray (C[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyUTF16IntoByteArray$default (C[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeAsciiChar (B)C public static final fun decodeAsciiChar ([BI)C public static final fun decodeUTF16Char ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)C public static final fun decodeUTF8Char (B)C public static final fun decodeUTF8Char ([BI)C - public static final fun encodeChar (Lcom/splendo/kaluga/base/bytes/Encoding;CLcom/splendo/kaluga/base/bytes/ByteOrder;)Ljava/io/Serializable; + public static final fun encodeChar (Lcom/splendo/kaluga/base/bytes/Encoding;CLcom/splendo/kaluga/base/bytes/ByteOrder;)[B public static final fun toAscii (C)B public static final fun toAsciiOrNull (C)Ljava/lang/Byte; public static final fun toUTF16 (CLcom/splendo/kaluga/base/bytes/ByteOrder;)[B } public final class com/splendo/kaluga/base/bytes/DoubleExtensionsKt { + public static final fun copyIntoByteArray (D[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray$default (D[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeDouble ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)D public static final fun toByteArray (DLcom/splendo/kaluga/base/bytes/ByteOrder;)[B } @@ -988,12 +1013,16 @@ public final class com/splendo/kaluga/base/bytes/Encoding : java/lang/Enum { } public final class com/splendo/kaluga/base/bytes/FloatExtensionsKt { + public static final fun copyIntoByteArray (F[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray$default (F[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeFloat ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)F public static final fun toByteArray (FLcom/splendo/kaluga/base/bytes/ByteOrder;)[B } public final class com/splendo/kaluga/base/bytes/Int24ExtensionsKt { public static final fun and-npgKo-Y (II)I + public static final fun copyIntoByteArray-w7mN1NY (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray-w7mN1NY$default (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeInt24 ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)I public static final fun isBitSet-QCWLtZ4 (ILjava/lang/Number;)Z public static final fun or-npgKo-Y (II)I @@ -1004,6 +1033,8 @@ public final class com/splendo/kaluga/base/bytes/Int24ExtensionsKt { } public final class com/splendo/kaluga/base/bytes/IntExtensionsKt { + public static final fun copyIntoByteArray (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray$default (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeInt ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)I public static final fun isBitSet (ILjava/lang/Number;)Z public static final fun length (I)I @@ -1019,6 +1050,8 @@ public final class com/splendo/kaluga/base/bytes/JAMCRC : com/splendo/kaluga/bas } public final class com/splendo/kaluga/base/bytes/LongExtensionsKt { + public static final fun copyIntoByteArray (J[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray$default (J[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeLong ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)J public static final fun isBitSet (JLjava/lang/Number;)Z public static final fun setBit (JLjava/lang/Number;)J @@ -1026,16 +1059,22 @@ public final class com/splendo/kaluga/base/bytes/LongExtensionsKt { } public final class com/splendo/kaluga/base/bytes/MedFloat16ExtensionsKt { + public static final fun copyIntoByteArray-Hl2vuUw (D[BI)[B + public static synthetic fun copyIntoByteArray-Hl2vuUw$default (D[BIILjava/lang/Object;)[B public static final fun decodeMedFloat16 ([BI)D public static final fun toByteArray-8th3mqE (D)[B } public final class com/splendo/kaluga/base/bytes/MedFloat32ExtensionsKt { + public static final fun copyIntoByteArray-DCrUOrk (D[BI)[B + public static synthetic fun copyIntoByteArray-DCrUOrk$default (D[BIILjava/lang/Object;)[B public static final fun decodeMedFloat32 ([BI)D public static final fun toByteArray-F5bENeA (D)[B } public final class com/splendo/kaluga/base/bytes/ShortExtensionsKt { + public static final fun copyIntoByteArray (S[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray$default (S[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeShort ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)S public static final fun isBitSet (SLjava/lang/Number;)Z public static final fun setBit (SLjava/lang/Number;)S @@ -1142,6 +1181,8 @@ public final class com/splendo/kaluga/base/bytes/UByteExtensionsKt { public final class com/splendo/kaluga/base/bytes/UInt24ExtensionsKt { public static final fun and-5oVDibg (II)I + public static final fun copyIntoByteArray-6sPYMpY (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray-6sPYMpY$default (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeUInt24 ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)I public static final fun isBitSet-593hS54 (ILjava/lang/Number;)Z public static final fun or-5oVDibg (II)I @@ -1152,6 +1193,8 @@ public final class com/splendo/kaluga/base/bytes/UInt24ExtensionsKt { } public final class com/splendo/kaluga/base/bytes/UIntExtensionsKt { + public static final fun copyIntoByteArray-SGjrQA4 (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray-SGjrQA4$default (I[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeUInt ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)I public static final fun isBitSet-qim9Vi0 (ILjava/lang/Number;)Z public static final fun setBit-qim9Vi0 (ILjava/lang/Number;)I @@ -1159,6 +1202,8 @@ public final class com/splendo/kaluga/base/bytes/UIntExtensionsKt { } public final class com/splendo/kaluga/base/bytes/ULongExtensionsKt { + public static final fun copyIntoByteArray-v3sQxsQ (J[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray-v3sQxsQ$default (J[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeULong ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)J public static final fun isBitSet-4PLdz1A (JLjava/lang/Number;)Z public static final fun setBit-4PLdz1A (JLjava/lang/Number;)J @@ -1166,6 +1211,8 @@ public final class com/splendo/kaluga/base/bytes/ULongExtensionsKt { } public final class com/splendo/kaluga/base/bytes/UShortExtensionsKt { + public static final fun copyIntoByteArray-k_pLkZQ (S[BILcom/splendo/kaluga/base/bytes/ByteOrder;)[B + public static synthetic fun copyIntoByteArray-k_pLkZQ$default (S[BILcom/splendo/kaluga/base/bytes/ByteOrder;ILjava/lang/Object;)[B public static final fun decodeUShort ([BILcom/splendo/kaluga/base/bytes/ByteOrder;)S public static final fun isBitSet-vckuEUM (SLjava/lang/Number;)Z public static final fun setBit-vckuEUM (SLjava/lang/Number;)S diff --git a/base/src/commonMain/kotlin/bytes/ByteArrayBuilder.kt b/base/src/commonMain/kotlin/bytes/ByteArrayBuilder.kt index c8fb60f07..fe791a0c3 100644 --- a/base/src/commonMain/kotlin/bytes/ByteArrayBuilder.kt +++ b/base/src/commonMain/kotlin/bytes/ByteArrayBuilder.kt @@ -22,6 +22,7 @@ import com.splendo.kaluga.base.utils.MedFloat16 import com.splendo.kaluga.base.utils.MedFloat32 import com.splendo.kaluga.base.utils.UInt24 import kotlin.experimental.or +import kotlin.math.min /** * Builds a [ByteArray] from primary types @@ -164,15 +165,23 @@ interface ByteArrayBuilder { /** * Builds a [ByteArray] using a [ByteArrayBuilder] * @param order the [ByteOrder] in which to add to the [ByteArray]. This is the default order in which elements will be encoded. + * @param expectedSize the initial size the ByteArray will use to approximate its final size. * @param block the building block using [ByteArrayBuilder] to build the array. * @return the built [ByteArray] */ -fun buildByteArray(order: ByteOrder = ByteOrder.LEAST_SIGNIFICANT_FIRST, block: ByteArrayBuilder.() -> Unit) = ByteArrayBuilderImpl( +fun buildByteArray(order: ByteOrder = ByteOrder.LEAST_SIGNIFICANT_FIRST, expectedSize: Int = Long.SIZE_BYTES, block: ByteArrayBuilder.() -> Unit) = ByteArrayBuilderImpl( + expectedSize, order, ).apply(block).build() -private class ByteArrayBuilderImpl(override val byteOrder: ByteOrder) : ByteArrayBuilder { - var bytes = byteArrayOf() +private class ByteArrayBuilderImpl(expectedSize: Int, override val byteOrder: ByteOrder) : ByteArrayBuilder { + + init { + require(expectedSize > 0) { "buildByteArray must have an expected size larger than 0" } + } + private val completedChunks = mutableListOf() + var currentChunk = ByteArray(expectedSize) + var currentByteOffset = 0 var currentByte: Byte = 0 var currentBit = 0 @@ -187,59 +196,115 @@ private class ByteArrayBuilderImpl(override val byteOrder: ByteOrder) : ByteArra } override fun add(byte: Byte) { - add(byteArrayOf(byte)) + add( + Byte.SIZE_BYTES, + generateIntoMethod = { currentChunk[it] = byte }, + generateMethod = { byteArrayOf(byte) }, + ) } override fun add(short: Short, order: ByteOrder) { - add(short.toByteArray(order)) + add( + Short.SIZE_BYTES, + generateIntoMethod = { short.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { short.toByteArray(order) }, + ) } override fun add(int24: Int24, order: ByteOrder) { - add(int24.toByteArray(order)) + add( + Int24.SIZE_BYTES, + generateIntoMethod = { int24.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { int24.toByteArray(order) }, + ) } override fun add(int: Int, order: ByteOrder) { - add(int.toByteArray(order)) + add( + Int.SIZE_BYTES, + generateIntoMethod = { int.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { int.toByteArray(order) }, + ) } override fun add(long: Long, order: ByteOrder) { - add(long.toByteArray(order)) + add( + Long.SIZE_BYTES, + generateIntoMethod = { long.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { long.toByteArray(order) }, + ) } override fun add(float: Float, order: ByteOrder) { - add(float.toByteArray(order)) + add( + Float.SIZE_BYTES, + generateIntoMethod = { float.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { float.toByteArray(order) }, + ) } override fun add(double: Double, order: ByteOrder) { - add(double.toByteArray(order)) + add( + Double.SIZE_BYTES, + generateIntoMethod = { double.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { double.toByteArray(order) }, + ) } override fun add(medFloat16: MedFloat16) { - add(medFloat16.toByteArray()) + add( + 2, + generateIntoMethod = { medFloat16.copyIntoByteArray(currentChunk, it) }, + generateMethod = { medFloat16.toByteArray() }, + ) } override fun add(medFloat32: MedFloat32) { - add(medFloat32.toByteArray()) + add( + 4, + generateIntoMethod = { medFloat32.copyIntoByteArray(currentChunk, it) }, + generateMethod = { medFloat32.toByteArray() }, + ) } override fun add(uByte: UByte) { - add(uByte.toByteArray()) + add( + UByte.SIZE_BYTES, + generateIntoMethod = { currentChunk[it] = uByte.toByte() }, + generateMethod = { uByte.toByteArray() }, + ) } override fun add(uShort: UShort, order: ByteOrder) { - add(uShort.toByteArray(order)) + add( + UShort.SIZE_BYTES, + generateIntoMethod = { uShort.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { uShort.toByteArray(order) }, + ) } override fun add(uInt: UInt, order: ByteOrder) { - add(uInt.toByteArray(order)) + add( + UInt.SIZE_BYTES, + generateIntoMethod = { uInt.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { uInt.toByteArray(order) }, + ) } override fun add(uLong: ULong, order: ByteOrder) { - add(uLong.toByteArray(order)) + add( + ULong.SIZE_BYTES, + generateIntoMethod = { uLong.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { uLong.toByteArray(order) }, + ) } override fun add(uInt24: UInt24, order: ByteOrder) { - add(uInt24.toByteArray(order)) + add( + UInt24.SIZE_BYTES, + generateIntoMethod = { uInt24.copyIntoByteArray(currentChunk, it, order) }, + generateMethod = { uInt24.toByteArray(order) }, + ) } override fun add(string: String, settings: StringEncodingSettings, order: ByteOrder) { @@ -247,32 +312,131 @@ private class ByteArrayBuilderImpl(override val byteOrder: ByteOrder) : ByteArra } override fun add(char: Char, encoding: Encoding, order: ByteOrder) { - add(char.toString(), StringEncodingSettings(StringEncodingSettings.NoMarking, encoding), order) + add( + encoding.byteSize, + generateIntoMethod = { encoding.copyCharIntoByteArray(char, currentChunk, it, order) }, + generateMethod = { encoding.encodeChar(char, order) }, + ) } override fun add(bytes: ByteArray) { + add( + bytes.size, + generateIntoMethod = { bytes.copyInto(currentChunk, it) }, + generateMethod = { bytes }, + ) + } + + private inline fun add(expectedSize: Int, generateIntoMethod: (Int) -> Unit, generateMethod: () -> ByteArray) { if (currentBit > 0) { addCurrentByte() } - when (byteOrder) { - ByteOrder.MOST_SIGNIFICANT_FIRST -> this.bytes = bytes + this.bytes - ByteOrder.LEAST_SIGNIFICANT_FIRST -> this.bytes += bytes + + val remaining = currentChunk.size - currentByteOffset + when { + expectedSize in 1.. { + generateIntoMethod( + when (byteOrder) { + ByteOrder.MOST_SIGNIFICANT_FIRST -> remaining - expectedSize + ByteOrder.LEAST_SIGNIFICANT_FIRST -> currentByteOffset + }, + ) + currentByteOffset += expectedSize + } + + else -> { + val fullSegment = generateMethod() + val splitIndex = min(fullSegment.size, remaining) + when (byteOrder) { + ByteOrder.LEAST_SIGNIFICANT_FIRST -> { + fullSegment.copyInto(currentChunk, currentByteOffset, 0, splitIndex) + currentByteOffset += splitIndex + if (splitIndex < fullSegment.size) { + completedChunks.add(currentChunk) + completedChunks.add(fullSegment.sliceArray(splitIndex.. { + fullSegment.copyInto(currentChunk, remaining - splitIndex, fullSegment.size - splitIndex) + currentByteOffset += splitIndex + if (splitIndex < fullSegment.size) { + completedChunks.add(currentChunk) + completedChunks.add(fullSegment.sliceArray(0.. this.bytes = byteArrayOf(currentByte) + this.bytes - ByteOrder.LEAST_SIGNIFICANT_FIRST -> this.bytes += currentByte + ByteOrder.MOST_SIGNIFICANT_FIRST -> currentChunk[currentChunk.size - currentByteOffset - 1] = currentByte + ByteOrder.LEAST_SIGNIFICANT_FIRST -> currentChunk[currentByteOffset] = currentByte } + currentByteOffset++ currentByte = 0 currentBit = 0 + checkChunkCompleted() + } + + private fun checkChunkCompleted() { + if (currentByteOffset == currentChunk.size) { + completedChunks.add(currentChunk) + currentChunk = ByteArray(currentChunk.size * 2) + currentByteOffset = 0 + } } fun build(): ByteArray { if (currentBit > 0) { addCurrentByte() } - return bytes + + return if (completedChunks.isEmpty()) { + if (currentByteOffset == 0) { + byteArrayOf() + } else { + when (byteOrder) { + ByteOrder.MOST_SIGNIFICANT_FIRST -> currentChunk.sliceArray(currentChunk.size - currentByteOffset.. currentChunk.sliceArray(0.. + when (byteOrder) { + ByteOrder.MOST_SIGNIFICANT_FIRST -> { + chunk.copyInto(bytes, totalSize - sumOfOffset - chunk.size) + } + + ByteOrder.LEAST_SIGNIFICANT_FIRST -> { + chunk.copyInto(bytes, sumOfOffset) + } + } + sumOfOffset + chunk.size + } + + if (currentByteOffset > 0) { + when (byteOrder) { + ByteOrder.MOST_SIGNIFICANT_FIRST -> { + currentChunk.copyInto(bytes, startIndex = currentChunk.size - currentByteOffset) + } + + ByteOrder.LEAST_SIGNIFICANT_FIRST -> { + currentChunk.copyInto(bytes, lastChunkOffset, endIndex = currentByteOffset) + } + } + } + + bytes + } } } diff --git a/base/src/commonMain/kotlin/bytes/CharExtensions.kt b/base/src/commonMain/kotlin/bytes/CharExtensions.kt index 091127a45..f9788e0f4 100644 --- a/base/src/commonMain/kotlin/bytes/CharExtensions.kt +++ b/base/src/commonMain/kotlin/bytes/CharExtensions.kt @@ -20,6 +20,7 @@ package com.splendo.kaluga.base.bytes import com.splendo.kaluga.base.bytes.Encoding.ASCII import com.splendo.kaluga.base.bytes.Encoding.UTF_16 import com.splendo.kaluga.base.bytes.Encoding.UTF_8 +import kotlin.coroutines.coroutineContext /** * Character encoding @@ -48,10 +49,33 @@ enum class Encoding(val byteSize: Int) { * @param char the [Char] to encode. * @param byteOrder the [ByteOrder] to use. For [Encoding] where [Encoding.byteSize] is 1, this can be ignored. */ -fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder) = when (this) { +fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder): ByteArray = when (this) { UTF_8 -> char.toString().encodeToByteArray() UTF_16 -> char.toUTF16(byteOrder) - ASCII -> char.toAscii() + ASCII -> byteArrayOf(char.toAscii()) +} + +/** + * Encodes a [Char] using the given [Encoding] and [ByteOrder] and copies it into a [ByteArray] at a given offset. + * @param char the [Char] to encode. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] in which the [Char] is encoded. For [Encoding] where [Encoding.byteSize] is 1, this can be ignored. + * @throws IllegalArgumentException if [array] is not is not large enough to hold [Encoding.byteSize] bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun Encoding.copyCharIntoByteArray(char: Char, array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray { + require(array.size > byteSize + offset) { "Cannot copy into ByteArray. Must be at least ${offset + byteSize} long" } + return when (this) { + UTF_8 -> char.toString().encodeToByteArray().copyInto(array, offset) + + UTF_16 -> char.copyUTF16IntoByteArray(array, offset, byteOrder) + + ASCII -> { + array[offset] = char.toAscii() + array + } + } } /** @@ -61,6 +85,16 @@ fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder) = when (this) { */ fun Char.toUTF16(byteOrder: ByteOrder): ByteArray = code.toUShort().toByteArray(byteOrder) +/** + * Encodes a [Char] and copies it into a [ByteArray] at a given offset in UTF-16 using the given [ByteOrder]. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] to use. + * @throws IllegalArgumentException if [array] is not is not large enough to hold 2 bytes at the [offset]. + * @return the [ByteArray] representing the [Char] in UTF-16. + */ +fun Char.copyUTF16IntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder) = code.toUShort().copyIntoByteArray(array, offset, byteOrder) + /** * Encodes a [Char] to a [Byte] in ASCII. * @throws IllegalArgumentException if the character cannot be represented in ASCII. Use [Char.toAsciiOrNull] to get a non-throwing variant diff --git a/base/src/commonMain/kotlin/bytes/DoubleExtensions.kt b/base/src/commonMain/kotlin/bytes/DoubleExtensions.kt index 3020062bc..91a2f72d3 100644 --- a/base/src/commonMain/kotlin/bytes/DoubleExtensions.kt +++ b/base/src/commonMain/kotlin/bytes/DoubleExtensions.kt @@ -34,3 +34,13 @@ fun ByteArray.decodeDouble(octetIndex: Int, byteOrder: ByteOrder): Double = Doub * @return [ByteArray] representing the [Double] */ fun Double.toByteArray(byteOrder: ByteOrder) = toRawBits().toByteArray(byteOrder) + +/** + * Encodes this [Double] and copies it into a [ByteArray] at a given offset. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] in which the [Double] is encoded + * @throws IllegalArgumentException if [array] is not is not large enough to hold 8 bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun Double.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray = toRawBits().copyIntoByteArray(array, offset, byteOrder) diff --git a/base/src/commonMain/kotlin/bytes/FloatExtensions.kt b/base/src/commonMain/kotlin/bytes/FloatExtensions.kt index f6e9746b4..545229b39 100644 --- a/base/src/commonMain/kotlin/bytes/FloatExtensions.kt +++ b/base/src/commonMain/kotlin/bytes/FloatExtensions.kt @@ -34,3 +34,13 @@ fun ByteArray.decodeFloat(octetIndex: Int, byteOrder: ByteOrder): Float = Float. * @return [ByteArray] representing the [Float] */ fun Float.toByteArray(byteOrder: ByteOrder) = toRawBits().toByteArray(byteOrder) + +/** + * Encodes this [Float] and copies it into a [ByteArray] at a given offset. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] in which the [Float] is encoded + * @throws IllegalArgumentException if [array] is not is not large enough to hold 4 bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun Float.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray = toRawBits().copyIntoByteArray(array, offset, byteOrder) diff --git a/base/src/commonMain/kotlin/bytes/Int24Extensions.kt b/base/src/commonMain/kotlin/bytes/Int24Extensions.kt index dfa78166c..12cb9014b 100644 --- a/base/src/commonMain/kotlin/bytes/Int24Extensions.kt +++ b/base/src/commonMain/kotlin/bytes/Int24Extensions.kt @@ -72,8 +72,22 @@ fun ByteArray.decodeInt24(octetIndex: Int, byteOrder: ByteOrder): Int24 { * @param byteOrder the [ByteOrder] in which the [Int24] is encoded * @return the encoded [ByteArray]. */ -fun Int24.toByteArray(byteOrder: ByteOrder) = ByteArray(Int24.SIZE_BYTES) { - (value shr byteOrder.shift(it, Int24.SIZE_BITS)).toByte() +fun Int24.toByteArray(byteOrder: ByteOrder) = copyIntoByteArray(ByteArray(Int24.SIZE_BYTES), byteOrder = byteOrder) + +/** + * Encodes this [Int24] and copies it into a [ByteArray] at a given offset. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] in which the [Int24] is encoded + * @throws IllegalArgumentException if [array] is not is not large enough to hold 3 bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun Int24.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray { + require(array.size >= offset + Int24.SIZE_BYTES) { "Cannot copy into ByteArray. Must be at least ${offset + Int24.SIZE_BYTES} long" } + for (index in 0..= offset + Int.SIZE_BYTES) { "Cannot copy into ByteArray. Must be at least ${offset + Int.SIZE_BYTES} long" } + for (index in 0..= offset + Long.SIZE_BYTES) { "Cannot copy into ByteArray. Must be at least ${offset + Long.SIZE_BYTES} long" } + for (index in 0..= offset + 2) { "Cannot copy into ByteArray. Must be at least ${offset + 2} long" } + if (value.isNaN()) return MedFloat16.NAN_BYTE_VALUE.copyInto(array, offset) + if (value == Double.POSITIVE_INFINITY) return MedFloat16.POSITIVE_INFINITY_BYTE_VALUE.copyInto(array, offset) + if (value == Double.NEGATIVE_INFINITY) return MedFloat16.NEGATIVE_INFINITY_BYTE_VALUE.copyInto(array, offset) var mantissa = value var exponent = 0 @@ -103,7 +113,7 @@ fun MedFloat16.toByteArray(): ByteArray { } if (mantissa.toInt() !in MedFloat16.MIN_MANTISSA..MedFloat16.MAX_MANTISSA) { - return MedFloat16.NOT_AT_THIS_RESOLUTION_BYTE_VALUE + return MedFloat16.NOT_AT_THIS_RESOLUTION_BYTE_VALUE.copyInto(array, offset) } val mant = mantissa.toInt() @@ -111,8 +121,7 @@ fun MedFloat16.toByteArray(): ByteArray { val raw = (exp shl 12) or (mant and 0x0FFF) - return byteArrayOf( - (raw and 0xFF).toByte(), - ((raw shr 8) and 0xFF).toByte(), - ) + array[offset] = (raw and 0xFF).toByte() + array[offset + 1] = ((raw shr 8) and 0xFF).toByte() + return array } diff --git a/base/src/commonMain/kotlin/bytes/MedFloat32Extensions.kt b/base/src/commonMain/kotlin/bytes/MedFloat32Extensions.kt index aa3a6a0b7..8b9d3721c 100644 --- a/base/src/commonMain/kotlin/bytes/MedFloat32Extensions.kt +++ b/base/src/commonMain/kotlin/bytes/MedFloat32Extensions.kt @@ -57,12 +57,22 @@ fun ByteArray.decodeMedFloat32(octetIndex: Int): MedFloat32 { * Encodes this [MedFloat32] into a [ByteArray]. * @return the encoded [ByteArray]. */ -fun MedFloat32.toByteArray(): ByteArray { - if (value.isNaN()) return MedFloat32.NAN_BYTE_VALUE +fun MedFloat32.toByteArray() = copyIntoByteArray(ByteArray(4)) - if (value == Double.POSITIVE_INFINITY) return MedFloat32.POSITIVE_INFINITY_BYTE_VALUE +/** + * Encodes this [MedFloat32] and copies it into a [ByteArray] at a given offset. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @throws IllegalArgumentException if [array] is not is not large enough to hold 4 bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun MedFloat32.copyIntoByteArray(array: ByteArray, offset: Int = 0): ByteArray { + require(array.size >= offset + 4) { "Cannot copy into ByteArray. Must be at least ${offset + 4} long" } + if (value.isNaN()) return MedFloat32.NAN_BYTE_VALUE.copyInto(array, offset) + + if (value == Double.POSITIVE_INFINITY) return MedFloat32.POSITIVE_INFINITY_BYTE_VALUE.copyInto(array, offset) - if (value == Double.NEGATIVE_INFINITY) return MedFloat32.NEGATIVE_INFINITY_BYTE_VALUE + if (value == Double.NEGATIVE_INFINITY) return MedFloat32.NEGATIVE_INFINITY_BYTE_VALUE.copyInto(array, offset) var mantissa = value var exponent = 0 @@ -87,15 +97,14 @@ fun MedFloat32.toByteArray(): ByteArray { } if (mantissa.toInt() !in Int24.MIN_VALUE.value..Int24.MAX_VALUE.value) { - return MedFloat32.NOT_AT_THIS_RESOLUTION_BYTE_VALUE + return MedFloat32.NOT_AT_THIS_RESOLUTION_BYTE_VALUE.copyInto(array, offset) } val mant = mantissa.toInt() - return byteArrayOf( - (mant and 0xFF).toByte(), - ((mant shr 8) and 0xFF).toByte(), - ((mant shr 16) and 0xFF).toByte(), - exponent.toByte(), - ) + array[offset] = (mant and 0xFF).toByte() + array[offset + 1] = ((mant shr 8) and 0xFF).toByte() + array[offset + 2] = ((mant shr 16) and 0xFF).toByte() + array[offset + 3] = exponent.toByte() + return array } diff --git a/base/src/commonMain/kotlin/bytes/ShortExtensions.kt b/base/src/commonMain/kotlin/bytes/ShortExtensions.kt index b37323e42..b024a10ec 100644 --- a/base/src/commonMain/kotlin/bytes/ShortExtensions.kt +++ b/base/src/commonMain/kotlin/bytes/ShortExtensions.kt @@ -40,8 +40,22 @@ fun ByteArray.decodeShort(octetIndex: Int, byteOrder: ByteOrder): Short { * @param byteOrder the [ByteOrder] in which the [Short] is encoded * @return the encoded [ByteArray]. */ -fun Short.toByteArray(byteOrder: ByteOrder) = ByteArray(Short.SIZE_BYTES) { - (this shr byteOrder.shift(it, Short.SIZE_BITS)).toByte() +fun Short.toByteArray(byteOrder: ByteOrder) = copyIntoByteArray(ByteArray(Short.SIZE_BYTES), byteOrder = byteOrder) + +/** + * Encodes this [Short] and copies it into a [ByteArray] at a given offset. + * @param array the [ByteArray] to copy the encoded data into. + * @param offset the offset at which to copy the encoded data. + * @param byteOrder the [ByteOrder] in which the [Short] is encoded + * @throws IllegalArgumentException if [array] is not is not large enough to hold 2 bytes at the [offset]. + * @return the encoded [ByteArray]. + */ +fun Short.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray { + require(array.size >= offset + Short.SIZE_BYTES) { "Cannot copy into ByteArray. Must be at least ${offset + Short.SIZE_BYTES} long" } + for (index in 0..= offset + UInt24.SIZE_BYTES) { "Cannot copy into ByteArray. Must be at least ${offset + UInt24.SIZE_BYTES} long" } + for (index in 0.. = setOf( + private val allProperties = setOf( Broadcast, Read, Write, @@ -452,7 +451,12 @@ sealed class CharacteristicProperty(val rawValue: Int, val encryptedValue: Int) Notify, Indicate, ExtendedProperties, - ).filter { + ) + + /** + * Gets a [Set] of [CharacteristicProperty] from an [Int] + */ + fun fromInt(properties: Int): Set = allProperties.filter { (properties and it.rawValue) != 0 || (properties and it.encryptedValue) != 0 }.toSet() } diff --git a/bluetooth/src/commonMain/kotlin/server/BluetoothServer.kt b/bluetooth/src/commonMain/kotlin/server/BluetoothServer.kt index b2bab9666..d4aff3777 100644 --- a/bluetooth/src/commonMain/kotlin/server/BluetoothServer.kt +++ b/bluetooth/src/commonMain/kotlin/server/BluetoothServer.kt @@ -235,8 +235,8 @@ class BluetoothServer internal constructor(private val settings: ServerSettings, private sealed class ServiceAction { class Add(val service: (ServerState.Available) -> LocalService, val isAdded: CompletableDeferred) : ServiceAction() - class Remove(val service: LocalService, val isRemoved: CompletableDeferred) : ServiceAction() - class RemoveAll(val isRemoved: CompletableDeferred) : ServiceAction() + class Remove(val service: LocalService, val isRemoved: EmptyCompletableDeferred) : ServiceAction() + class RemoveAll(val isRemoved: EmptyCompletableDeferred) : ServiceAction() } private class NotifyingAction( @@ -594,34 +594,28 @@ class BluetoothServer internal constructor(private val settings: ServerSettings, throw e } - withContext(NonCancellable) { - if (didAdd) { - logger.warn(TAG) { "Added service ${service.uuid}" } - _services.update { it + service } - serviceAction.isAdded.complete(service) - } else { - logger.warn(TAG) { "Failed to add service ${service.uuid}" } - serviceAction.isAdded.complete(null) - } + if (didAdd) { + logger.warn(TAG) { "Added service ${service.uuid}" } + _services.update { it + service } + serviceAction.isAdded.complete(service) + } else { + logger.warn(TAG) { "Failed to add service ${service.uuid}" } + serviceAction.isAdded.complete(null) } } is ServiceAction.Remove -> { - withContext(NonCancellable) { - available.removeService(serviceAction.service) - _services.update { it - serviceAction.service } - disconnectAllConnectedDevices(serviceAction.service) - serviceAction.isRemoved.complete() - } + available.removeService(serviceAction.service) + _services.update { it - serviceAction.service } + disconnectAllConnectedDevices(serviceAction.service) + serviceAction.isRemoved.complete() } is ServiceAction.RemoveAll -> { - withContext(NonCancellable) { - available.removeAllServices() - _services.value = emptyList() - disconnectAllConnectedDevices() - serviceAction.isRemoved.complete() - } + available.removeAllServices() + _services.value = emptyList() + disconnectAllConnectedDevices() + serviceAction.isRemoved.complete() } } } diff --git a/bluetooth/src/commonMain/kotlin/server/LocalCharacteristic.kt b/bluetooth/src/commonMain/kotlin/server/LocalCharacteristic.kt index 45727e208..1fa06166f 100644 --- a/bluetooth/src/commonMain/kotlin/server/LocalCharacteristic.kt +++ b/bluetooth/src/commonMain/kotlin/server/LocalCharacteristic.kt @@ -18,6 +18,7 @@ package com.splendo.kaluga.bluetooth.server import com.splendo.kaluga.base.collections.concurrentMutableMapOf +import com.splendo.kaluga.base.utils.EmptyCompletableDeferred import com.splendo.kaluga.bluetooth.Characteristic import com.splendo.kaluga.bluetooth.CharacteristicProperty import com.splendo.kaluga.bluetooth.Descriptor @@ -357,7 +358,7 @@ sealed class LocalCharacteristic(val wrapper: LocalCharacteristicWrapper, overri * @param notification the [NotificationDSL] to use to set up notification */ fun StateFlow.collectTo(scope: CoroutineScope, notification: NotificationDSL.() -> Unit) { - val hasStarted = CompletableDeferred() + val hasStarted = EmptyCompletableDeferred() NotificationDSL( this@DSL, onSubscribe = { device, toByteArray -> @@ -385,7 +386,7 @@ sealed class LocalCharacteristic(val wrapper: LocalCharacteristicWrapper, overri * @param notification the [NotificationDSL] to use to set up notification */ fun ReceiveChannel.consumeTo(scope: CoroutineScope, notification: NotificationDSL.() -> Unit) { - val hasStarted = CompletableDeferred() + val hasStarted = EmptyCompletableDeferred() NotificationDSL( this@DSL, onSubscribe = { device, toByteArray -> diff --git a/bluetooth/src/iosMain/kotlin/server/KalugaCBPeripheralManagerDelegate.kt b/bluetooth/src/iosMain/kotlin/server/KalugaCBPeripheralManagerDelegate.kt index 6231fe3da..355b44781 100644 --- a/bluetooth/src/iosMain/kotlin/server/KalugaCBPeripheralManagerDelegate.kt +++ b/bluetooth/src/iosMain/kotlin/server/KalugaCBPeripheralManagerDelegate.kt @@ -17,6 +17,7 @@ package com.splendo.kaluga.bluetooth.server +import com.splendo.kaluga.base.utils.EmptyCompletableDeferred import com.splendo.kaluga.base.utils.complete import com.splendo.kaluga.base.utils.toNSData import com.splendo.kaluga.base.utils.typedList @@ -25,6 +26,8 @@ import com.splendo.kaluga.bluetooth.asBytes import com.splendo.kaluga.logging.Logger import com.splendo.kaluga.logging.info import com.splendo.kaluga.logging.warn +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock import kotlinx.cinterop.ObjCSignatureOverride import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineName @@ -64,7 +67,7 @@ class KalugaCBPeripheralManagerDelegate(private val logger: Logger, handlingCont return didStartAdvertising } - private var available = CompletableDeferred() + private var available = EmptyCompletableDeferred() fun resetAvailable(): Deferred { if (!available.isCompleted) { available = CompletableDeferred() @@ -72,6 +75,8 @@ class KalugaCBPeripheralManagerDelegate(private val logger: Logger, handlingCont return available } + private val lock = reentrantLock() + private val readActions = mutableMapOf GattResponse.ReadResponse>() private val writeActions = mutableMapOf GattResponse.WriteResponse>() private val subscribeActions = mutableMapOf Unit>() @@ -79,7 +84,7 @@ class KalugaCBPeripheralManagerDelegate(private val logger: Logger, handlingCont private val handlingScope = CoroutineScope(handlingContext + CoroutineName("CBPeripheralManagerDelegate")) - fun registerReadAction(characteristic: LocalCharacteristic, onRead: suspend LocalCharacteristic.(ConnectedDevice, Int) -> GattResponse.ReadResponse) { + fun registerReadAction(characteristic: LocalCharacteristic, onRead: suspend LocalCharacteristic.(ConnectedDevice, Int) -> GattResponse.ReadResponse) = lock.withLock { val identifier = characteristic.wrapper.characteristic if (readActions.contains(identifier)) { logger.warn(TAG) { "Read action for $identifier was already set. Ignoring" } @@ -88,16 +93,17 @@ class KalugaCBPeripheralManagerDelegate(private val logger: Logger, handlingCont } } - fun registerWriteAction(characteristic: LocalCharacteristic, onWrite: suspend LocalCharacteristic.(ConnectedDevice, ByteArray, Int) -> GattResponse.WriteResponse) { - val identifier = characteristic.wrapper.characteristic - if (writeActions.contains(identifier)) { - logger.warn(TAG) { "Write action for $identifier was already set. Ignoring" } - } else { - writeActions[identifier] = { device, offset, value -> characteristic.onWrite(device, offset, value) } + fun registerWriteAction(characteristic: LocalCharacteristic, onWrite: suspend LocalCharacteristic.(ConnectedDevice, ByteArray, Int) -> GattResponse.WriteResponse) = + lock.withLock { + val identifier = characteristic.wrapper.characteristic + if (writeActions.contains(identifier)) { + logger.warn(TAG) { "Write action for $identifier was already set. Ignoring" } + } else { + writeActions[identifier] = { device, offset, value -> characteristic.onWrite(device, offset, value) } + } } - } - fun registerSubscriptionActions(characteristic: LocalCharacteristic.Notifiable) { + fun registerSubscriptionActions(characteristic: LocalCharacteristic.Notifiable) = lock.withLock { val identifier = characteristic.wrapper.characteristic when { subscribeActions.contains(identifier) -> logger.warn(TAG) { "Subscribe action for $identifier was already set. Ignoring" } @@ -113,12 +119,14 @@ class KalugaCBPeripheralManagerDelegate(private val logger: Logger, handlingCont fun removeService(service: CBService) { service.includedServices.orEmpty().typedList().forEach(::removeService) - readActions -= readActions.keys.filter { it.service == service.UUID }.toSet() - writeActions -= writeActions.keys.filter { it.service == service.UUID }.toSet() - subscribeActions -= subscribeActions.keys.filter { it.service == service.UUID }.toSet() + lock.withLock { + readActions -= readActions.keys.filter { it.service == service.UUID }.toSet() + writeActions -= writeActions.keys.filter { it.service == service.UUID }.toSet() + subscribeActions -= subscribeActions.keys.filter { it.service == service.UUID }.toSet() + } } - fun removeAllServices() { + fun removeAllServices() = lock.withLock { readActions.clear() writeActions.clear() subscribeActions.clear() diff --git a/date-time/src/commonTest/kotlin/com/splendo/kaluga/datetime/timer/RecurringTimerTest.kt b/date-time/src/commonTest/kotlin/com/splendo/kaluga/datetime/timer/RecurringTimerTest.kt index 29804e42c..466a83689 100644 --- a/date-time/src/commonTest/kotlin/com/splendo/kaluga/datetime/timer/RecurringTimerTest.kt +++ b/date-time/src/commonTest/kotlin/com/splendo/kaluga/datetime/timer/RecurringTimerTest.kt @@ -17,9 +17,9 @@ package com.splendo.kaluga.datetime.timer import com.splendo.kaluga.base.runBlocking +import com.splendo.kaluga.base.utils.EmptyCompletableDeferred import com.splendo.kaluga.test.base.assertEmits import com.splendo.kaluga.test.base.captureFor -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -168,7 +168,7 @@ class RecurringTimerTest { /** Validates requested delays. */ private class PredefinedDelayHandler(val delays: List) { - private val timerFinish = CompletableDeferred() + private val timerFinish = EmptyCompletableDeferred() private var index = -1 // -1 to capture overall timer finish delay suspend fun delay(delay: Duration) {