TalerBleProtocol.kt

/*
 * This file is part of GNU Taler
 * (C) 2026 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.merchantpos.devices

import java.util.UUID

internal object TalerBleProtocol
{
    const val NAME = "Taler NFC Terminal"
    val SERVICE: UUID = UUID.fromString("04942ad3-8352-4da9-b0e8-22323c7f18b7")
    val URL: UUID = UUID.fromString("829f9e88-206d-47f0-bd07-ada608deaa67")
    val CONTROL: UUID = UUID.fromString("829f9e88-206d-47f0-bd07-ada608deaa68")
    val STATUS: UUID = UUID.fromString("829f9e88-206d-47f0-bd07-ada608deaa69")

    // The physical LCD also ships with the SIG URI characteristic in the same Taler service.
    val STANDARD_URI: UUID = UUID.fromString("00002ab6-0000-1000-8000-00805f9b34fb")
    const val TEST_URL = "https://taler.net/#display-test"
    const val CAPACITY = 255

    fun isBondedFirmwareName(name: String?) = name?.matches(Regex("Taler [0-9A-Fa-f]{6}")) == true

    fun isCandidate(
        name: String?,
        services: List<UUID>,
    ) = name == NAME || isBondedFirmwareName(name) || SERVICE in services
}

/** One acknowledged write at a time. Never split URLs: firmware replaces on every write. */
internal class TalerBleUrlQueue
{
    private var acknowledged: String? = null
    private var inFlight: String? = null

    private val target get() = if (tooLong) "" else desired

    var desired = ""
    var mtu = 23

    val tooLong get() = desired.toByteArray(Charsets.UTF_8).size > minOf(TalerBleProtocol.CAPACITY, mtu - 3)

    val settled get() = inFlight == null && acknowledged == target

    val writing get() = inFlight != null

    fun next(): ByteArray?
    {
        if (inFlight != null ||
            acknowledged == target)
        {
            return null
        }
        inFlight = target
        return target.toByteArray(Charsets.UTF_8)
    }

    fun complete(success: Boolean)
    {
        acknowledged = if (success) inFlight else null
        inFlight = null
    }

    fun reset()
    {
        acknowledged = null
        inFlight = null
        mtu = 23
    }
}