Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions iosApp/iosApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,20 @@
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Required to discover and connect to Epson printers via Bluetooth.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Required to discover and connect to Epson printers on the local network.</string>
<key>NSBonjourServices</key>
<array>
<string>_printer._tcp</string>
<string>_pdl-datastream._tcp</string>
<string>_ipp._tcp</string>
<string>_ipps._tcp</string>
</array>
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.epson.escpos</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -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<DiscoveryState>
get() = TODO("Not yet implemented")
get() = callbackFlow {
trySend(DiscoveryState.Idle)

val discoveredPrinters = mutableMapOf<String, ExternalPrinter>()

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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fun PrinterLayout(
printState: PrinterState,
printers: List<ExternalPrinter>,
onPrinterSelected: (ExternalPrinter?) -> Unit,
discoveryStatus: String? = null,
) {
Column(
modifier = Modifier.fillMaxSize(),
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -34,8 +37,32 @@ fun MainViewController(): UIViewController = ComposeUIViewController {

val printers = remember { mutableStateListOf<ExternalPrinter>() }
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)

Expand All @@ -46,6 +73,7 @@ fun MainViewController(): UIViewController = ComposeUIViewController {
) {
PrinterLayout(
printState = printState,
discoveryStatus = discoveryStatus,
printers = printers,
onPrinterSelected = { selectedPrinter ->
scope.launch {
Expand All @@ -56,6 +84,7 @@ fun MainViewController(): UIViewController = ComposeUIViewController {
}
initialized = true
} catch (_: Exception) {
discoveryStatus = "Failed to initialize printer"
return@launch
}
}
Expand Down
Loading