diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index b37a1ff..2fca73a 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -9,5 +9,20 @@ UIApplicationSupportsMultipleScenes + NSBluetoothAlwaysUsageDescription + Required to discover and connect to Epson printers via Bluetooth. + NSLocalNetworkUsageDescription + Required to discover and connect to Epson printers on the local network. + NSBonjourServices + + _printer._tcp + _pdl-datastream._tcp + _ipp._tcp + _ipps._tcp + + UISupportedExternalAccessoryProtocols + + com.epson.escpos + diff --git a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt index 08c0553..133bfac 100644 --- a/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt +++ b/print-plugins/epson/src/iosMain/kotlin/de/tillhub/printengine/epson/EpsonPrinterDiscovery.ios.kt @@ -1,10 +1,102 @@ package de.tillhub.printengine.epson +import com.epson.epos2.Epos2DeviceInfo +import com.epson.epos2.Epos2Discovery +import com.epson.epos2.Epos2DiscoveryDelegateProtocol +import com.epson.epos2.Epos2FilterOption +import de.tillhub.printengine.data.ConnectionType import de.tillhub.printengine.data.DiscoveryState +import de.tillhub.printengine.data.ExternalPrinter +import de.tillhub.printengine.data.PrinterInfo +import de.tillhub.printengine.data.PrinterServiceVersion +import de.tillhub.printengine.data.PrintingFontType +import de.tillhub.printengine.data.PrintingPaperSpec import de.tillhub.printengine.external.PrinterDiscovery +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.withContext +import platform.darwin.NSObject +@OptIn(ExperimentalForeignApi::class) actual class EpsonPrinterDiscovery : PrinterDiscovery { + + private companion object { + const val CHARACTER_COUNT = 42 + const val MANUFACTURER_EPSON = "EPSON" + + // ePOS2.h enum values + const val EPOS2_SUCCESS = 0 + const val EPOS2_PORTTYPE_ALL = 0 + const val EPOS2_TYPE_PRINTER = 1 + const val EPOS2_MODEL_ALL = 0 + } + actual override val observePrinters: Flow - get() = TODO("Not yet implemented") + get() = callbackFlow { + trySend(DiscoveryState.Idle) + + val discoveredPrinters = mutableMapOf() + + val filterOption = Epos2FilterOption().apply { + portType = EPOS2_PORTTYPE_ALL + deviceType = EPOS2_TYPE_PRINTER + deviceModel = EPOS2_MODEL_ALL + } + + // Epson SDK dispatches onDiscovery callbacks sequentially on its internal thread + val discoveryDelegate = object : NSObject(), Epos2DiscoveryDelegateProtocol { + override fun onDiscovery(deviceInfo: Epos2DeviceInfo?) { + val info = deviceInfo ?: return + if (!info.isValid()) return + val printer = createPrinter(info) + discoveredPrinters[printer.connectionAddress] = printer + trySend(DiscoveryState.Discovering(discoveredPrinters.values.toList())) + } + } + + val result = withContext(Dispatchers.Main) { + Epos2Discovery.start(filterOption, discoveryDelegate) + } + if (result != EPOS2_SUCCESS) { + trySend(DiscoveryState.Error("Discovery start failed with code: $result")) + } + + awaitClose { + Epos2Discovery.stop() + // Reference delegate to prevent Kotlin/Native GC from collecting it + // while the Epson SDK still holds a weak reference to it + discoveryDelegate.hashCode() + } + } + + private fun createPrinter(deviceInfo: Epos2DeviceInfo): ExternalPrinter { + val target = deviceInfo.target ?: "" + val dividerIdx = target.indexOf(':') + val protocol = if (dividerIdx >= 0) target.substring(0, dividerIdx) else "" + val address = if (dividerIdx >= 0) target.substring(dividerIdx + 1) else target + + return ExternalPrinter( + info = PrinterInfo( + serialNumber = "n/a", + deviceModel = deviceInfo.deviceName ?: "", + printerVersion = "n/a", + printerPaperSpec = PrintingPaperSpec.External(CHARACTER_COUNT), + printingFontType = PrintingFontType.DEFAULT_FONT_SIZE, + printerHead = "n/a", + printedDistance = 0, + serviceVersion = PrinterServiceVersion.Unknown, + ), + manufacturer = MANUFACTURER_EPSON, + connectionAddress = address, + connectionType = protocol.toConnectionType(), + ) + } + + private fun String.toConnectionType(): ConnectionType = ConnectionType.entries.find { it.value == this } + ?: ConnectionType.LAN + + private fun Epos2DeviceInfo.isValid(): Boolean = deviceType == EPOS2_TYPE_PRINTER && !deviceName.isNullOrEmpty() } diff --git a/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt b/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt index 506f1f6..924e16b 100644 --- a/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt +++ b/sample/src/commonMain/kotlin/de/tillhub/printengine/sample/ui/PrinterLayout.kt @@ -30,6 +30,7 @@ fun PrinterLayout( printState: PrinterState, printers: List, onPrinterSelected: (ExternalPrinter?) -> Unit, + discoveryStatus: String? = null, ) { Column( modifier = Modifier.fillMaxSize(), @@ -41,6 +42,14 @@ fun PrinterLayout( textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge, ) + if (discoveryStatus != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = discoveryStatus, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + ) + } Spacer(modifier = Modifier.height(36.dp)) Button(onClick = { onPrinterSelected(null) }) { Text(text = "Print sample job") diff --git a/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt b/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt index 3be48d9..890ab7c 100644 --- a/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt +++ b/sample/src/iosMain/kotlin/de/tillhub/printengine/sample/MainViewController.kt @@ -3,6 +3,7 @@ package de.tillhub.printengine.sample import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -13,11 +14,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.window.ComposeUIViewController import de.tillhub.printengine.PrintEngine +import de.tillhub.printengine.data.DiscoveryState import de.tillhub.printengine.data.ExternalPrinter import de.tillhub.printengine.data.PrintCommand import de.tillhub.printengine.data.PrintJob import de.tillhub.printengine.data.PrinterState import de.tillhub.printengine.data.PrintingIntensity +import de.tillhub.printengine.epson.EpsonPrinterDiscovery import de.tillhub.printengine.epson.EpsonServiceProvider import de.tillhub.printengine.sample.ui.PrinterLayout import de.tillhub.printengine.sample.ui.theme.TillhubPrintEngineTheme @@ -34,8 +37,32 @@ fun MainViewController(): UIViewController = ComposeUIViewController { val printers = remember { mutableStateListOf() } var initialized by remember { mutableStateOf(false) } + var discoveryStatus by remember { mutableStateOf("Searching for printers…") } val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + printEngine.discoverExternalPrinters( + EpsonPrinterDiscovery(), + ).collect { state -> + when (state) { + is DiscoveryState.Idle -> discoveryStatus = "Searching for printers…" + is DiscoveryState.Discovering -> { + printers.clear() + printers.addAll(state.printers) + discoveryStatus = "Found ${state.printers.size} printer(s)" + } + is DiscoveryState.Finished -> { + printers.clear() + printers.addAll(state.printers) + discoveryStatus = "Discovery done: ${state.printers.size} printer(s)" + } + is DiscoveryState.Error -> { + discoveryStatus = "Discovery error: ${state.message}" + } + } + } + } + val printState by printEngine.printer.printerState .collectAsState(PrinterState.CheckingForPrinter) @@ -46,6 +73,7 @@ fun MainViewController(): UIViewController = ComposeUIViewController { ) { PrinterLayout( printState = printState, + discoveryStatus = discoveryStatus, printers = printers, onPrinterSelected = { selectedPrinter -> scope.launch { @@ -56,6 +84,7 @@ fun MainViewController(): UIViewController = ComposeUIViewController { } initialized = true } catch (_: Exception) { + discoveryStatus = "Failed to initialize printer" return@launch } }