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
}
}