From 706c912b139f8630db7d7de78554da6bd89b66c2 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Mon, 30 Mar 2026 22:02:49 +0100 Subject: [PATCH] Simplified BluetoothReporter and NetworkReporter --- .../core/data/framework/BluetoothReporter.kt | 163 +++++++++--------- .../core/data/framework/NetworkReporter.kt | 130 +++++++------- .../core/data/injection/MainContainer.kt | 25 ++- .../core/domain/repository/IMainContainer.kt | 4 +- .../core/domain/repository/IReporterRepo.kt | 17 +- .../look4sat/feature/radar/RadarViewModel.kt | 54 ++---- 6 files changed, 179 insertions(+), 214 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/BluetoothReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/BluetoothReporter.kt index 9a41963e..b36387e2 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/BluetoothReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/BluetoothReporter.kt @@ -20,121 +20,112 @@ package com.rtbishop.look4sat.core.data.framework import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothSocket import android.util.Log -import com.rtbishop.look4sat.core.domain.repository.BtService -import com.rtbishop.look4sat.core.domain.repository.IReporterRepo -import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams +import com.rtbishop.look4sat.core.domain.repository.IReporter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.OutputStream import java.util.UUID -import kotlin.math.abs - -data class DeviceConnection( - var socket: BluetoothSocket? = null, - var outputStream: OutputStream? = null, - var connected: Boolean = false, - var connecting: Boolean = false, - var connectionJob: Job? = null -) class BluetoothReporter( private val bluetoothManager: BluetoothManager, - private val reporterScope: CoroutineScope -) : IReporterRepo { + private val reporterScope: CoroutineScope, + private val rotatorDeviceId: String, + private val frequencyDeviceId: String +) : IReporter { private val tag = "BTReporter" - private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") - - private val serviceToDevice = mutableMapOf() - private val deviceConnections = mutableMapOf() + private val sppId: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") private val writeMutex = Mutex() - override fun isConnected(service: BtService): Boolean { - val deviceId = serviceToDevice[service] ?: return false - return deviceConnections[deviceId]?.connected == true - } + private var rotatorSocket: BluetoothSocket? = null + private var rotatorStream: OutputStream? = null + private var rotatorConnected = false + private var rotatorConnecting = false - override fun isConnecting(service: BtService): Boolean { - val deviceId = serviceToDevice[service] ?: return false - return deviceConnections[deviceId]?.connecting == true - } + private var frequencySocket: BluetoothSocket? = null + private var frequencyStream: OutputStream? = null + private var frequencyConnected = false + private var frequencyConnecting = false - override fun connect(service: BtService, deviceId: String) { - serviceToDevice[service] = deviceId - val connection = deviceConnections.getOrPut(deviceId) { - DeviceConnection() + override fun reportRotation(format: String, azimuth: Double, elevation: Double) { + reporterScope.launch { + ensureRotatorConnected() + if (!rotatorConnected) return@launch + val el = if (elevation > 0.0) elevation else 0.0 + val command = format + .replace($$"$AZ", "%03d".format(azimuth.toInt())) + .replace($$"$EL", "%03d".format(el.toInt())) + .unescapeControlChars() + write(rotatorStream, command) { rotatorConnected = false } } - if (connection.connected || connection.connecting) return - connection.connectionJob = reporterScope.launch { + } + + override fun reportFrequency(format: String, frequency: Long) { + reporterScope.launch { + ensureFrequencyConnected() + if (!frequencyConnected) return@launch + val command = format + .replace($$"$FREQ", frequency.toString()) + .unescapeControlChars() + write(frequencyStream, command) { frequencyConnected = false } + } + } + + private fun ensureRotatorConnected() { + if (rotatorConnected || rotatorConnecting || rotatorDeviceId.isBlank()) return + reporterScope.launch { try { - connection.connecting = true - val device = bluetoothManager.adapter.getRemoteDevice(deviceId) - val socket = device.createInsecureRfcommSocketToServiceRecord(sppid) + rotatorConnecting = true + val device = bluetoothManager.adapter.getRemoteDevice(rotatorDeviceId) + val socket = device.createInsecureRfcommSocketToServiceRecord(sppId) socket.connect() - connection.socket = socket - connection.outputStream = socket.outputStream - connection.connected = true - Log.i(tag, "$tag: Connected to $deviceId") + rotatorSocket = socket + rotatorStream = socket.outputStream + rotatorConnected = true + Log.i(tag, "Rotator connected to $rotatorDeviceId") } catch (e: Exception) { - Log.e(tag, "$tag: ${e.message}") - connection.connected = false + Log.e(tag, "Rotator connect error: ${e.message}") + rotatorConnected = false } finally { - connection.connecting = false + rotatorConnecting = false } } } - private suspend fun write(service: BtService, buffer: String) { - val deviceId = serviceToDevice[service] ?: return - val connection = deviceConnections[deviceId] ?: return - if (!connection.connected) return + private fun ensureFrequencyConnected() { + if (frequencyConnected || frequencyConnecting || frequencyDeviceId.isBlank()) return + reporterScope.launch { + try { + frequencyConnecting = true + val device = bluetoothManager.adapter.getRemoteDevice(frequencyDeviceId) + val socket = device.createInsecureRfcommSocketToServiceRecord(sppId) + socket.connect() + frequencySocket = socket + frequencyStream = socket.outputStream + frequencyConnected = true + Log.i(tag, "Frequency connected to $frequencyDeviceId") + } catch (e: Exception) { + Log.e(tag, "Frequency connect error: ${e.message}") + frequencyConnected = false + } finally { + frequencyConnecting = false + } + } + } + + private suspend fun write(stream: OutputStream?, data: String, onError: () -> Unit) { try { writeMutex.withLock { - connection.outputStream?.write(buffer.toByteArray()) + stream?.write(data.toByteArray()) } } catch (e: Exception) { - Log.e(tag, "$tag: Write failed ${e.message}") - connection.connected = false + Log.e(tag, "Write error: ${e.message}") + onError() } } - override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: WithoutExtParams) { - reporterScope.launch { - if (!isConnected(BtService.ROTATOR)) return@launch - val newElevation = if (elevation > 0.0) elevation else 0.0 - val azimuthString = intToStringWithLeadingZeroes(azimuth.toInt()) - val elevationString = intToStringWithLeadingZeroes(newElevation.toInt()) - val buffer = format - .replace($$"$AZ", azimuthString) - .replace($$"$EL", elevationString) - .replace("\\r", "\r") - .replace("\\n", "\n") - .replace("\\t", "\t") - write(BtService.ROTATOR, buffer) - } - } - - override fun reportFrequency(format: String, frequency: Long, params: WithoutExtParams) { - reporterScope.launch { - if (!isConnected(BtService.FREQUENCY)) return@launch - val buffer = format - .replace($$"$FREQ", frequency.toString()) - .replace("\\r", "\r") - .replace("\\n", "\n") - .replace("\\t", "\t") - write(BtService.FREQUENCY, buffer) - } - } - - private fun intToStringWithLeadingZeroes(value: Int): String { - return if (value > 0) { - if (value < 10) "00$value" else if (value < 100) "0$value" else "$value" - } else { - val absValue = abs(value) - if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue" - } - } + private fun String.unescapeControlChars(): String = + replace("\\r", "\r").replace("\\n", "\n").replace("\\t", "\t") } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/NetworkReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/NetworkReporter.kt index 6dd1f5f2..e6cb3773 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/NetworkReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/framework/NetworkReporter.kt @@ -17,11 +17,8 @@ */ package com.rtbishop.look4sat.core.data.framework -import com.rtbishop.look4sat.core.domain.repository.BtService -import com.rtbishop.look4sat.core.domain.repository.ExtendedParams -import com.rtbishop.look4sat.core.domain.repository.IReporterRepo +import com.rtbishop.look4sat.core.domain.repository.IReporter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -29,89 +26,88 @@ import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SocketChannel -enum class NetworkService { ROTATOR, FREQUENCY } +class NetworkReporter( + private val reporterScope: CoroutineScope, + private val rotatorServer: String, + private val rotatorPort: Int, + private val frequencyServer: String, + private val frequencyPort: Int +) : IReporter { -data class SocketConnection( - var socket: SocketChannel? = null, - var connected: Boolean = false, - var connecting: Boolean = false, - var connectionJob: Job? = null -) - -class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo { - private val serviceToAddress = mutableMapOf() - private val connections = mutableMapOf() private val writeMutex = Mutex() - fun isConnected(service: NetworkService): Boolean { - val addr = serviceToAddress[service] ?: return false - return connections[addr]?.connected == true + private var rotatorSocket: SocketChannel? = null + private var rotatorConnected = false + private var rotatorConnecting = false + + private var frequencySocket: SocketChannel? = null + private var frequencyConnected = false + private var frequencyConnecting = false + + override fun reportRotation(format: String, azimuth: Double, elevation: Double) { + reporterScope.launch { + ensureRotatorConnected() + if (!rotatorConnected) return@launch + val el = if (elevation > 0.0) elevation else 0.0 + val command = format + .replace($$"$AZ", azimuth.toString()) + .replace($$"$EL", el.toString()) + write(rotatorSocket, command) { rotatorConnected = false } + } } - private fun getOrCreateConnection(addr: String): SocketConnection { - return connections.getOrPut(addr) { SocketConnection() } + override fun reportFrequency(format: String, frequency: Long) { + reporterScope.launch { + ensureFrequencyConnected() + if (!frequencyConnected) return@launch + val command = format.replace($$"$FREQ", frequency.toString()) + write(frequencySocket, command) { frequencyConnected = false } + } } - fun connect(service: NetworkService, server: String, port: Int) { - val addr = "$server:$port" - serviceToAddress[service] = addr - val connection = getOrCreateConnection(addr) - if (connection.connected || connection.connecting) return - connection.connectionJob = reporterScope.launch { + private fun ensureRotatorConnected() { + if (rotatorConnected || rotatorConnecting || rotatorServer.isBlank()) return + reporterScope.launch { try { - connection.connecting = true - val socket = SocketChannel.open(InetSocketAddress(server, port)) - connection.socket = socket - connection.connected = true - println("NetworkReporter: $service connected to $addr") + rotatorConnecting = true + rotatorSocket = SocketChannel.open(InetSocketAddress(rotatorServer, rotatorPort)) + rotatorConnected = true + println("NetworkReporter: Rotator connected to $rotatorServer:$rotatorPort") } catch (e: Exception) { - println("NetworkReporter connect error: ${e.message}") - connection.connected = false + println("NetworkReporter rotator connect error: ${e.message}") + rotatorConnected = false } finally { - connection.connecting = false + rotatorConnecting = false } } } - private suspend fun write(service: NetworkService, command: String) { - val addr = serviceToAddress[service] ?: return - val connection = connections[addr] ?: return - if (!connection.connected) return + private fun ensureFrequencyConnected() { + if (frequencyConnected || frequencyConnecting || frequencyServer.isBlank()) return + reporterScope.launch { + try { + frequencyConnecting = true + frequencySocket = SocketChannel.open(InetSocketAddress(frequencyServer, frequencyPort)) + frequencyConnected = true + println("NetworkReporter: Frequency connected to $frequencyServer:$frequencyPort") + } catch (e: Exception) { + println("NetworkReporter frequency connect error: ${e.message}") + frequencyConnected = false + } finally { + frequencyConnecting = false + } + } + } + + private suspend fun write(socket: SocketChannel?, command: String, onError: () -> Unit) { try { writeMutex.withLock { val buffer = ByteBuffer.wrap("\\$command\n".toByteArray()) - connection.socket?.write(buffer) + socket?.write(buffer) } } catch (e: Exception) { println("NetworkReporter write error: ${e.message}") - connection.connected = false - } - } - - override fun isConnected(service: BtService): Boolean = false - - override fun isConnecting(service: BtService): Boolean = false - - override fun connect(service: BtService, deviceId: String) {} - - override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: ExtendedParams) { - reporterScope.launch { - connect(NetworkService.ROTATOR, params.server, params.port) - if (!isConnected(NetworkService.ROTATOR)) return@launch - val el = if (elevation > 0.0) elevation else 0.0 - val command = format - .replace("\$AZ", azimuth.toString()) - .replace("\$EL", el.toString()) - write(NetworkService.ROTATOR, command) - } - } - - override fun reportFrequency(format: String, frequency: Long, params: ExtendedParams) { - reporterScope.launch { - connect(NetworkService.FREQUENCY, params.server, params.port) - if (!isConnected(NetworkService.FREQUENCY)) return@launch - val command = format.replace("\$FREQ", frequency.toString()) - write(NetworkService.FREQUENCY, command) + onError() } } } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt index 1b15b246..235e3396 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt @@ -19,15 +19,13 @@ import com.rtbishop.look4sat.core.data.source.LocalSource import com.rtbishop.look4sat.core.data.source.RemoteSource import com.rtbishop.look4sat.core.data.usecase.AddToCalendar import com.rtbishop.look4sat.core.data.usecase.ShowToast -import com.rtbishop.look4sat.core.domain.repository.ExtendedParams import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.core.domain.repository.IMainContainer -import com.rtbishop.look4sat.core.domain.repository.IReporterRepo +import com.rtbishop.look4sat.core.domain.repository.IReporter import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo -import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams import com.rtbishop.look4sat.core.domain.source.ILocalSource import com.rtbishop.look4sat.core.domain.source.IRemoteSource import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar @@ -53,13 +51,26 @@ class MainContainer(private val context: Context) : IMainContainer { override fun provideShowToast(): IShowToast = ShowToast(context) - override fun provideBluetoothReporter(): IReporterRepo { + override fun provideBluetoothReporter(): IReporter { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO)) + val rc = settingsRepo.rcSettings.value + return BluetoothReporter( + manager, + CoroutineScope(Dispatchers.IO), + rc.bluetoothRotatorAddress, + rc.bluetoothFrequencyAddress + ) } - override fun provideNetworkReporter(): IReporterRepo { - return NetworkReporter(CoroutineScope(Dispatchers.IO)) + override fun provideNetworkReporter(): IReporter { + val rc = settingsRepo.rcSettings.value + return NetworkReporter( + CoroutineScope(Dispatchers.IO), + rc.rotatorAddress, + rc.rotatorPort.toIntOrNull() ?: 0, + rc.frequencyAddress, + rc.frequencyPort.toIntOrNull() ?: 0 + ) } override fun provideSensorsRepo(): ISensorsRepo { diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt index 0c60403c..d811f61d 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt @@ -12,8 +12,8 @@ interface IMainContainer { val databaseRepo: IDatabaseRepo fun provideAddToCalendar(): IAddToCalendar fun provideShowToast(): IShowToast - fun provideBluetoothReporter(): IReporterRepo - fun provideNetworkReporter(): IReporterRepo + fun provideBluetoothReporter(): IReporter + fun provideNetworkReporter(): IReporter fun provideSensorsRepo(): ISensorsRepo } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IReporterRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IReporterRepo.kt index 9f050f3c..b8edf7f5 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IReporterRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IReporterRepo.kt @@ -17,18 +17,7 @@ */ package com.rtbishop.look4sat.core.domain.repository -interface IReporterParams - -interface IReporterRepo { - fun isConnected(service: BtService): Boolean - fun isConnecting(service: BtService): Boolean - fun connect(service: BtService, deviceId: String) - fun reportRotation(format: String, azimuth: Double, elevation: Double, params: T) - fun reportFrequency(format: String, frequency: Long, params: T) +interface IReporter { + fun reportRotation(format: String, azimuth: Double, elevation: Double) + fun reportFrequency(format: String, frequency: Long) } - -data class ExtendedParams(val server: String, val port: Int) : IReporterParams - -data class WithoutExtParams(val dummy: Int) : IReporterParams - -enum class BtService { ROTATOR, FREQUENCY } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 5757bd7d..0bae67c7 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -28,14 +28,11 @@ import com.rtbishop.look4sat.core.domain.model.SatRadio import com.rtbishop.look4sat.core.domain.predict.OrbitalObject import com.rtbishop.look4sat.core.domain.predict.OrbitalPass import com.rtbishop.look4sat.core.domain.predict.OrbitalPos -import com.rtbishop.look4sat.core.domain.repository.BtService -import com.rtbishop.look4sat.core.domain.repository.ExtendedParams import com.rtbishop.look4sat.core.domain.repository.IContainerProvider -import com.rtbishop.look4sat.core.domain.repository.IReporterRepo +import com.rtbishop.look4sat.core.domain.repository.IReporter import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo -import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar import com.rtbishop.look4sat.core.domain.utility.round import com.rtbishop.look4sat.core.domain.utility.toDegrees @@ -49,8 +46,8 @@ import kotlinx.coroutines.launch class RadarViewModel( private val savedStateHandle: SavedStateHandle, - private val bluetoothReporter: IReporterRepo, - private val networkReporter: IReporterRepo, + private val bluetoothReporter: IReporter, + private val networkReporter: IReporter, private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo, private val sensorsRepo: ISensorsRepo, @@ -143,20 +140,15 @@ class RadarViewModel( } _uiState.update { it.copy(orbitalPos = orbitalPos, satTrack = track) } viewModelScope.launch { - if (settingsRepo.rcSettings.value.rotatorState) { - val server = settingsRepo.rcSettings.value.rotatorAddress - val port = settingsRepo.rcSettings.value.rotatorPort.toInt() - val format = settingsRepo.rcSettings.value.rotatorFormat + val rc = settingsRepo.rcSettings.value + if (rc.rotatorState) { val azimuth = orbitalPos.azimuth.toDegrees().round(2) val elevation = orbitalPos.elevation.toDegrees().round(2) - networkReporter.reportRotation(format, azimuth, elevation, ExtendedParams(server, port)) + networkReporter.reportRotation(rc.rotatorFormat, azimuth, elevation) } - if (settingsRepo.rcSettings.value.frequencyState) { + if (rc.frequencyState) { _uiState.value.selectedFrequency?.let { freq -> - val server = settingsRepo.rcSettings.value.frequencyAddress - val port = settingsRepo.rcSettings.value.frequencyPort.toInt() - val format = settingsRepo.rcSettings.value.frequencyFormat - networkReporter.reportFrequency(format,freq, ExtendedParams(server, port)) + networkReporter.reportFrequency(rc.frequencyFormat, freq) } } } @@ -164,29 +156,15 @@ class RadarViewModel( private fun sendPassDataBT(orbitalPos: OrbitalPos) { viewModelScope.launch { - if (settingsRepo.rcSettings.value.bluetoothRotatorState) { - val btRotatorDevice = settingsRepo.rcSettings.value.bluetoothRotatorAddress - if (bluetoothReporter.isConnected(BtService.ROTATOR)) { - val format = settingsRepo.rcSettings.value.bluetoothRotatorFormat - val azimuth = orbitalPos.azimuth.toDegrees().round(0) - val elevation = orbitalPos.elevation.toDegrees().round(0) - bluetoothReporter.reportRotation(format, azimuth, elevation, WithoutExtParams(0)) - } else if (!bluetoothReporter.isConnecting(BtService.ROTATOR)) { -// Log.i("BTReporter", "BTReporter (rotator): Attempting to connect...") - bluetoothReporter.connect(BtService.ROTATOR,btRotatorDevice) - } + val rc = settingsRepo.rcSettings.value + if (rc.bluetoothRotatorState) { + val azimuth = orbitalPos.azimuth.toDegrees().round(0) + val elevation = orbitalPos.elevation.toDegrees().round(0) + bluetoothReporter.reportRotation(rc.bluetoothRotatorFormat, azimuth, elevation) } - if (settingsRepo.rcSettings.value.bluetoothFrequencyState) { - val btFrequencyDevice = settingsRepo.rcSettings.value.bluetoothFrequencyAddress - if (bluetoothReporter.isConnected(BtService.FREQUENCY)) { - val format = settingsRepo.rcSettings.value.bluetoothFrequencyFormat - val frequency = _uiState.value.selectedFrequency - frequency?.let { freq -> - bluetoothReporter.reportFrequency(format, freq, WithoutExtParams(0)) - } - } else if (!bluetoothReporter.isConnecting(BtService.FREQUENCY)) { -// Log.i("BTReporter", "BTReporter (frequency): Attempting to connect...") - bluetoothReporter.connect(BtService.FREQUENCY, btFrequencyDevice) + if (rc.bluetoothFrequencyState) { + _uiState.value.selectedFrequency?.let { freq -> + bluetoothReporter.reportFrequency(rc.bluetoothFrequencyFormat, freq) } } }