Android library for controlling the ethOS terminal screen and the 3x3 LED array via a clean, coroutine-based Kotlin API. Both subsystems communicate with hidden system services through reflection and gracefully no-op on non-ethOS hardware.
- Installation
- Quick Start
- Terminal Buttons
- LED Patterns
- Flash Patterns
- Custom LED Grid
- Building Custom Terminal Screens
- Custom Bitmaps
- Terminal Screen Power Control
- Lifecycle Management
- Architecture Patterns for Production Apps
- API Reference
- Requirements
- Contributing
Step 1. Add the JitPack repository to your project's settings.gradle.kts (inside the dependencyResolutionManagement block):
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}If your project uses a root
build.gradle.ktswithallprojects { repositories { ... } }instead, add the maven line there.
Step 2. Add the dependency to your app module's build.gradle.kts:
dependencies {
implementation("com.github.EthereumPhone:TerminalSDK:0.1.0")
}Running from source: If you've cloned or forked this repo, include the SDK module directly in
settings.gradle.kts:include(":TerminalSDK")Then reference it from your app module:
dependencies { implementation(project(":TerminalSDK")) }
class MainActivity : ComponentActivity() {
private var terminal: TerminalSDK? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialise — safe on non-ethOS devices (display/led will be null)
terminal = TerminalSDK(this)
// Show a terminal button on the terminal screen
lifecycleScope.launch {
terminal?.showQrOrSend(
onQrCode = { /* user tapped the QR area */ },
onSend = { /* user tapped the Send area */ }
)
}
// Flash the success LED pattern
terminal?.led?.flashSuccess()
}
override fun onPause() {
super.onPause()
// Restore the default terminal screen
lifecycleScope.launch { terminal?.dismissDisplay() }
}
override fun onDestroy() {
super.onDestroy()
// Release all resources
terminal?.destroy()
}
}The terminal screen is a 428x142 pixel display on the back of the ethOS device. The SDK ships with pre-built button layouts that render as bitmaps and register touch handlers automatically.
// QR Scan / Send TXN — left half triggers onQrCode, right half triggers onSend
terminal?.showQrOrSend(
onQrCode = { /* open QR scanner */ },
onSend = { /* open send flow */ }
)
// QR Scan / Send NFT — same split, right half triggers onSendNft
terminal?.showSendNft(
onQrCode = { /* open QR scanner */ },
onSendNft = { /* open NFT send flow */ }
)Each renders a full-width button and fires the callback on any tap:
terminal?.showCopy { /* copy address to clipboard */ }
terminal?.showCopiedAddress { /* show copied confirmation */ }
terminal?.showTopUp { /* open top-up flow */ }
terminal?.showSwap { /* open swap flow */ }
terminal?.showLog { /* open block explorer */ }
terminal?.showDetailLog { /* open TX detail in explorer */ }Display arbitrary text on a black background (no touch handler):
terminal?.showBlackText("SENDING TX...")
terminal?.showBlackText("CONFIRMING...")Restore the default system display (status bar) and remove touch handlers:
terminal?.dismissDisplay()All
show*methods aresuspendfunctions. Call them from a coroutine scope (e.g.lifecycleScope.launch { ... }).
The 3x3 LED array on the back of the device supports a library of built-in patterns. Each accepts an optional hex colour string — when omitted, the system accent colour is used.
val led = terminal?.led
// Branding
led?.displayChad() // ethOS logo, system accent colour
led?.displayChad("#FF00FF") // ethOS logo, custom magenta
// Directional
led?.displayPlus() // + symbol
led?.displayMinus() // − symbol
led?.displaySend() // arrow up ↑
led?.displayReceive() // arrow down ↓
led?.displaySwap() // swap indicator
// Status feedback
led?.displaySuccess() // green checkmark ✓
led?.displayError() // red cross ✗
led?.displayWarning() // yellow warning ⚠
led?.displayInfo() // info indicator ℹ
// Signing
led?.displaySign() // signing indicator
// Clear all LEDs
led?.clear()You can also display a pattern dynamically by its string name:
led?.displayPattern("success", "#00FF00")
led?.displayPattern("chad")
// Available names:
// "chad", "plus", "minus", "success", "error", "warning",
// "info", "arrowup", "arrowdown", "swap", "sign"val patterns: List<String> = led?.getAvailablePatterns() ?: emptyList()
// ["chad", "plus", "minus", "success", "error", "warning",
// "info", "arrowup", "arrowdown", "swap", "sign"]Flash patterns display a status indicator briefly (default 1 second), then automatically revert to the chad branding pattern. Useful for confirming actions like successful transactions or errors.
// Flash success for 1 second, then show chad
led?.flashSuccess()
// Flash error for 2 seconds with a custom colour
led?.flashError(color = "#FF0000", durationMs = 2000L)
// All flash variants:
led?.flashSuccess(color = null, durationMs = 1000L)
led?.flashError(color = null, durationMs = 1000L)
led?.flashWarning(color = null, durationMs = 1000L)
led?.flashInfo(color = null, durationMs = 1000L)// 1. Show "sending" on the terminal screen + send pattern on LEDs
terminal?.showBlackText("SENDING TX...")
led?.displaySend()
// 2. Wait for the transaction result...
val result = sendTransaction(...)
// 3. Flash success or error based on outcome
if (result.isSuccess) {
led?.flashSuccess()
terminal?.showBlackText("TX CONFIRMED")
} else {
led?.flashError()
terminal?.showBlackText("TX FAILED")
}
// 4. After a delay, return to normal
delay(2000)
terminal?.dismissDisplay()Beyond predefined patterns, you have full control over individual LEDs in the 3x3 grid.
Hardware IDs: Grid coordinates:
0 1 2 [0,0] [0,1] [0,2]
3 4 5 [1,0] [1,1] [1,2]
6 7 8 [2,0] [2,1] [2,2]
// Set LED at row 0, column 1 to green
led?.setColor(0, 1, "#00FF00")
// Set LED with brightness (0–8)
led?.setColor(1, 1, "#FF0000", brightness = 4)// All LEDs blue at default brightness (6)
led?.setAllColor("#0000FF")
// All LEDs white at half brightness
led?.setAllColor("#FFFFFF", brightness = 3)Pass a 3x3 array of hex colour strings. Use "#000000" for off:
// X pattern
led?.setCustomPattern(arrayOf(
arrayOf("#FF0000", "#000000", "#FF0000"),
arrayOf("#000000", "#FF0000", "#000000"),
arrayOf("#FF0000", "#000000", "#FF0000"),
))
// Diamond pattern with brightness
led?.setCustomPattern(
pattern = arrayOf(
arrayOf("#000000", "#00FFFF", "#000000"),
arrayOf("#00FFFF", "#000000", "#00FFFF"),
arrayOf("#000000", "#00FFFF", "#000000"),
),
brightness = 8
)// Global brightness (0–8)
led?.setBrightness(5)
// Adjust colour brightness programmatically (0–100%)
val dimRed = led?.applyBrightness("#FF0000", 50) // returns "0x7F0000"The SDK accepts colours in any of these formats — they're normalised internally:
| Format | Example |
|---|---|
#RRGGBB |
"#FF0000" |
#AARRGGBB |
"#FFFF0000" |
0xRRGGBB |
"0xFF0000" |
0xAARRGGBB |
"0xFFFF0000" |
The SDK ships with pre-built button layouts, but you can design your own terminal screen with custom buttons, icons, text, and touch regions. The terminal screen is 428x142 pixels — everything you show is an XML layout rendered into a bitmap.
- Create an XML layout sized to 428x142 px
- Render it into a
BitmapusingLayoutRenderer(or manually) - Push the bitmap to the terminal screen with
showOnDisplay() - Register a touch handler to make regions interactive
Create a layout file in your app's res/layout/ directory. The root must be 428px wide and 142px high with a black background to match the terminal screen:
<!-- res/layout/my_custom_terminal.xml -->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="428px"
android:layout_height="142px"
android:background="#000000">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Left button: APPROVE -->
<ImageView
android:id="@+id/approve_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/approve_label"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.25" />
<TextView
android:id="@+id/approve_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="APPROVE"
android:textColor="#00FF00"
android:fontFamily="@font/monomaniac"
android:textSize="17sp"
android:textAllCaps="true"
android:letterSpacing="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/approve_icon"
app:layout_constraintTop_toTopOf="parent" />
<!-- Right button: REJECT -->
<ImageView
android:id="@+id/reject_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/reject_label"
app:layout_constraintStart_toEndOf="@+id/approve_label"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.75" />
<TextView
android:id="@+id/reject_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="REJECT"
android:textColor="#FF0000"
android:fontFamily="@font/monomaniac"
android:textSize="17sp"
android:textAllCaps="true"
android:letterSpacing="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/reject_icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>Design tips:
- Always use a
#000000background (OLED-friendly, matches the device) - Use the included
@font/monomaniacfont for the authentic terminal aesthetic, or apply the@style/terminal_textstyle - Keep text uppercase with letter spacing for readability at small sizes
- Use the system accent colour for icons/labels (see Step 2)
- The ConstraintLayout dependency is already included in the SDK
Inflate your layout, apply the system accent colour, and convert it to a bitmap:
fun renderCustomLayout(context: Context): Bitmap {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.my_custom_terminal, null)
// Apply the system accent colour to icons and labels
val accentColor = Settings.Secure.getInt(
context.contentResolver,
"systemui_accent_color",
0xFFFF0000.toInt() // default red
)
view.findViewById<ImageView>(R.id.approve_icon)
?.setColorFilter(accentColor, PorterDuff.Mode.SRC_IN)
view.findViewById<TextView>(R.id.approve_label)
?.setTextColor(accentColor)
view.findViewById<ImageView>(R.id.reject_icon)
?.setColorFilter(accentColor, PorterDuff.Mode.SRC_IN)
view.findViewById<TextView>(R.id.reject_label)
?.setTextColor(accentColor)
// Measure and layout at the terminal screen resolution
val width = 428
val height = 142
val wSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val hSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
view.measure(wSpec, hSpec)
view.layout(0, 0, width, height)
// Draw into a bitmap
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}Shortcut: You can also use the SDK's built-in
LayoutRendereras a reference or extend it. The renderer interminal.rendereralready handles inflation, measuring, accent colouring, and bitmap conversion for the built-in layouts.
Push the bitmap to the terminal screen and register touch zones:
lifecycleScope.launch {
val bitmap = renderCustomLayout(context)
// Display the custom screen with a split touch handler
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, y, action ->
if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener
// Split the 428px width into left/right tap zones
if (x < 214f) {
// Left half — APPROVE tapped
handleApprove()
} else {
// Right half — REJECT tapped
handleReject()
}
})
}Always dismiss your custom screen when leaving:
// Restore the default terminal screen
terminal?.dismissDisplay()Putting it all together in a ViewModel-driven flow:
class SigningViewModel(
private val terminal: TerminalSDK?,
private val context: Context
) : ViewModel() {
fun showApprovalScreen(onApprove: () -> Unit, onReject: () -> Unit) {
viewModelScope.launch {
val bitmap = renderCustomLayout(context)
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, _, action ->
if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener
if (x < 214f) onApprove() else onReject()
})
// Show the sign LED pattern while waiting for user input
terminal?.led?.displaySign()
}
}
fun dismissApprovalScreen() {
viewModelScope.launch {
terminal?.dismissDisplay()
terminal?.led?.displayChad()
}
}
}You're not limited to a left/right split. Divide the 428px width into any number of zones:
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, _, action ->
if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener
when {
x < 143f -> handleLeftButton() // first third
x < 286f -> handleMiddleButton() // middle third
else -> handleRightButton() // last third
}
})If you prefer to skip XML entirely, draw directly onto a Canvas:
fun renderProgrammatic(): Bitmap {
val bitmap = Bitmap.createBitmap(428, 142, Bitmap.Config.RGB_565)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.BLACK)
val paint = Paint().apply {
color = Color.GREEN
textSize = 36f
isAntiAlias = true
typeface = Typeface.MONOSPACE
textAlign = Paint.Align.CENTER
}
canvas.drawText("CONNECTED", 214f, 82f, paint)
return bitmap
}For any fully custom content, render any Bitmap to the terminal screen:
val bitmap: Bitmap = ... // your 428x142 bitmap
// Display with no touch handler
terminal?.showOnDisplay(bitmap)
// Display with a touch handler
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, y, action ->
if (action == MotionEvent.ACTION_DOWN) {
if (x < 214f) {
// Left half tapped
} else {
// Right half tapped
}
}
})You can also use the built-in LayoutRenderer to render the SDK's pre-built layouts manually:
val renderer = terminal?.renderer
val bitmap = renderer?.renderQrOrSend()
val bitmap = renderer?.renderBlackText("CUSTOM MESSAGE")Control the terminal screen power state directly:
// Turn the terminal screen on/off
terminal?.display?.screenOn()
terminal?.display?.screenOff()
// Check current state
val isOn: Boolean = terminal?.display?.isScreenOn() ?: falseProper lifecycle management prevents resource leaks and ensures the terminal screen returns to its default state when your app is backgrounded or destroyed.
class MyActivity : ComponentActivity() {
private var terminal: TerminalSDK? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
terminal = TerminalSDK(this)
}
override fun onPause() {
super.onPause()
// Restore the system display when app loses focus
lifecycleScope.launch {
terminal?.dismissDisplay()
}
}
override fun onDestroy() {
super.onDestroy()
// Release all resources — clears LEDs, cancels coroutines,
// destroys touch handlers
terminal?.destroy()
}
}Use DisposableEffect tied to the composable lifecycle:
@Composable
fun TerminalAwareScreen(terminal: TerminalSDK?) {
val scope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
scope.launch { terminal?.showCopy { /* handle tap */ } }
terminal?.led?.displayChad()
}
Lifecycle.Event.ON_PAUSE -> {
scope.launch { terminal?.dismissDisplay() }
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}If you use the SDK from a ViewModel, clean up in onCleared():
class MyViewModel(
private val terminal: TerminalSDK?
) : ViewModel() {
fun showSendButton() {
viewModelScope.launch {
terminal?.showQrOrSend(
onQrCode = { /* ... */ },
onSend = { /* ... */ }
)
}
}
override fun onCleared() {
super.onCleared()
terminal?.destroy()
}
}For larger apps (like the ethOS Wallet Manager), structure your terminal interactions through a repository layer with dependency injection.
Wrap the SDK in a repository to decouple UI from hardware control and emit touch events as flows:
sealed interface TerminalEvent {
object CopyTapped : TerminalEvent
object QrTapped : TerminalEvent
object SendTapped : TerminalEvent
object SwapTapped : TerminalEvent
object TopUpTapped : TerminalEvent
object LogTapped : TerminalEvent
}
@Singleton
class TerminalRepository @Inject constructor(
private val terminal: TerminalSDK?,
@ApplicationContext private val context: Context
) {
private val _events = MutableSharedFlow<TerminalEvent>()
val events: SharedFlow<TerminalEvent> = _events
private val scope = CoroutineScope(Dispatchers.IO)
suspend fun showSendButtons() {
terminal?.showQrOrSend(
onQrCode = { scope.launch { _events.emit(TerminalEvent.QrTapped) } },
onSend = { scope.launch { _events.emit(TerminalEvent.SendTapped) } }
)
}
suspend fun showCopy() {
terminal?.showCopy {
scope.launch { _events.emit(TerminalEvent.CopyTapped) }
}
}
suspend fun dismiss() {
terminal?.dismissDisplay()
}
}Provide the SDK as a nullable singleton — it will be null on non-ethOS devices:
@Module
@InstallIn(SingletonComponent::class)
object TerminalModule {
@Provides
@Singleton
fun provideTerminalSDK(
@ApplicationContext context: Context
): TerminalSDK? {
return try {
val sdk = TerminalSDK(context)
if (sdk.isAvailable) sdk else null
} catch (e: Exception) {
null
}
}
}@HiltViewModel
class SendViewModel @Inject constructor(
private val terminalRepository: TerminalRepository
) : ViewModel() {
init {
// React to terminal touch events
viewModelScope.launch {
terminalRepository.events.collect { event ->
when (event) {
TerminalEvent.QrTapped -> openQrScanner()
TerminalEvent.SendTapped -> navigateToAmountInput()
else -> {}
}
}
}
}
fun onScreenVisible() {
viewModelScope.launch {
terminalRepository.showSendButtons()
}
}
fun onScreenHidden() {
viewModelScope.launch {
terminalRepository.dismiss()
}
}
}Update the terminal screen as the user navigates between screens:
@Composable
fun SendScreen(
terminal: TerminalSDK?,
viewModel: SendViewModel = hiltViewModel()
) {
val scope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
// Update terminal screen when this screen gains/loses focus
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> viewModel.onScreenVisible()
Lifecycle.Event.ON_PAUSE -> viewModel.onScreenHidden()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// ... your UI ...
}| Property / Method | Description |
|---|---|
display: TerminalDisplay? |
Terminal screen controller (null if unavailable) |
led: TerminalLED? |
LED array controller (null if unavailable) |
renderer: LayoutRenderer |
Renders XML layouts to bitmaps for the terminal screen |
isAvailable: Boolean |
true if either display or LED is available |
isDisplayAvailable: Boolean |
true if the terminal screen is available |
isLedAvailable: Boolean |
true if the LED array is available |
showQrOrSend(onQrCode, onSend) |
Show QR/Send dual-button (suspend) |
showSendNft(onQrCode, onSendNft) |
Show QR/Send NFT dual-button (suspend) |
showCopy(onCopy) |
Show Copy button (suspend) |
showCopiedAddress(onTap) |
Show Copied confirmation (suspend) |
showTopUp(onTopUp) |
Show Top Up button (suspend) |
showLog(onLog) |
Show View on Explorer button (suspend) |
showDetailLog(onDetailLog) |
Show View TX in Explorer button (suspend) |
showSwap(onSwap) |
Show Swap button (suspend) |
showBlackText(text) |
Show text on black background (suspend) |
showOnDisplay(bitmap, touchListener?) |
Push any bitmap to the terminal screen (suspend) |
dismissDisplay() |
Restore default terminal screen, remove touch handlers (suspend) |
destroy() |
Release all resources — call in onDestroy() |
| Method | Description |
|---|---|
isAvailable: Boolean |
Whether the LED subsystem is available |
displayChad(color?) |
Show ethOS branding pattern |
displayPlus(color?) / displayMinus(color?) |
Show +/− pattern |
displaySend(color?) / displayReceive(color?) |
Show arrow up/down pattern |
displaySwap(color?) / displaySign(color?) |
Show swap/sign pattern |
displaySuccess(color?) |
Show success checkmark |
displayError(color?) |
Show error cross |
displayWarning(color?) |
Show warning symbol |
displayInfo(color?) |
Show info symbol |
displayPattern(name, color?) |
Show pattern by string name |
flashSuccess(color?, durationMs?) |
Flash success then revert to chad |
flashError(color?, durationMs?) |
Flash error then revert to chad |
flashWarning(color?, durationMs?) |
Flash warning then revert to chad |
flashInfo(color?, durationMs?) |
Flash info then revert to chad |
setColor(row, col, color) |
Set single LED colour (row/col 0-2) |
setColor(row, col, color, brightness) |
Set single LED with brightness (0-8) |
setAllColor(color, brightness?) |
Set all LEDs to one colour |
setCustomPattern(pattern, brightness?) |
Set arbitrary 3x3 colour pattern |
setBrightness(brightness) |
Set global brightness (0-8) |
applyBrightness(hexColor, brightnessPercent) |
Adjust colour brightness (0-100%) |
getAvailablePatterns(): List<String> |
List all predefined pattern names |
getSystemColor(): String? |
Get cached system accent colour |
refreshSystemColor() |
Re-read the system accent colour |
clear() |
Turn off all LEDs |
destroy() |
Clear LEDs and cancel pending coroutines |
| Method | Description |
|---|---|
isAvailable(): Boolean |
Whether the terminal screen is available (suspend) |
isScreenOn(): Boolean |
Check if terminal screen is on (suspend) |
screenOn() / screenOff() |
Power the terminal screen on/off (suspend) |
refresh(bitmap, layerId): Boolean |
Push a bitmap to a display layer (suspend) |
resume(layerId) |
Restore a display layer (suspend) |
registerTouchListener(listener) |
Register a touch callback |
destroyTouchHandler() |
Remove the current touch callback (suspend) |
destroyTouchHandlerSync() |
Remove touch callback synchronously |
finish() |
Restore status bar + destroy touch handler |
| Constant | Description |
|---|---|
ID_STATUSBAR |
System status bar layer |
ID_INCOMINGCALL |
Incoming call layer |
ID_NOTIFICATIONS |
Notification layer |
ID_CLOCK |
Clock layer |
ID_GOOGLEBYE |
Always-on display (AOD) layer |
ID_PERSISTENT |
Persistent layer (stays until replaced) |
- Android API 33+ (minSdk)
- ethOS device for hardware features — the SDK gracefully no-ops on standard Android devices
- Kotlin Coroutines — display methods are suspend functions
- Fork or clone the repository
- Open the project in Android Studio
- The
appmodule is a demo app that exercises every SDK feature — run it on an ethOS device to test - The
TerminalSDKmodule is the library — make your changes there - Submit a pull request
TerminalSDK/
├── app/ # Demo / sample application
│ └── src/main/java/.../
│ └── MainActivity.kt # Full interactive demo
├── TerminalSDK/ # Library module
│ └── src/main/java/.../
│ ├── TerminalSDK.kt # Main entry point
│ ├── display/
│ │ ├── TerminalDisplay.kt # Terminal screen controller
│ │ ├── LayoutRenderer.java # XML → Bitmap renderer
│ │ └── MiniDisplayTouchHandler.java # Touch event handling
│ └── led/
│ ├── TerminalLED.kt # High-level LED controller
│ ├── LedPattern.kt # Built-in pattern definitions
│ └── LedManager.kt # Low-level LED proxy
└── README.md
{% embed url="https://github.com/EthereumPhone/TerminalSDK" %}