Initial commit, adding original binaries, initial watch and companion apps which can talk

This commit is contained in:
ml
2026-06-01 15:53:11 +02:00
commit d7adb56197
63 changed files with 1787 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
/*.txt
*.iml
.gradle
.DS_Store
.externalNativeBuild
.cxx
local.properties
.lock*
/SchwertUndMagieOnPebbleCompanionApp/local.properties
/SchwertUndMagieOnPebbleCompanionApp/.idea/caches
/SchwertUndMagieOnPebbleCompanionApp/.idea/libraries
/SchwertUndMagieOnPebbleCompanionApp/.idea/modules.xml
/SchwertUndMagieOnPebbleCompanionApp/.idea/workspace.xml
/SchwertUndMagieOnPebbleCompanionApp/.idea/navEditor.xml
/SchwertUndMagieOnPebbleCompanionApp/.idea/assetWizardSettings.xml
/SchwertUndMagieOnPebbleCompanionApp/build
/SchwertUndMagieOnPebbleCompanionApp/captures
/SchwertUndMagieOnPebbleWatchApp/build
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-31T15:36:41.858231245Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/ml/.android/avd/Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
@@ -0,0 +1 @@
/build
@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "de.ladkau.schwertundmagieonpebblecompanionapp"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "de.ladkau.schwertundmagieonpebblecompanionapp"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core.ktx)
implementation(libs.material)
implementation(libs.nanohttpd)
implementation(libs.pebblekit)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
}
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("de.ladkau.schwertundmagieonpebblecompanionapp", appContext.packageName)
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SchwertUndMagieOnPebbleCompanionApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,37 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.graphics.Bitmap
import java.nio.ByteBuffer
class C64Engine {
// Native hooks representing underlying open-source emulator loops (e.g., Frodo Core)
external fun initEmulator(romPath: String): Boolean
external fun runFrame()
external fun getRawVideoBuffer(buffer: ByteBuffer) // Populates native 320x200 ARGB pixels
external fun injectNativeKey(keyCode: Int, isPressed: Boolean)
companion object {
init {
System.loadLibrary("frodoc64_core")
}
}
// Maps standard incoming string commands to traditional C64 matrix keycodes
fun handleWatchInput(command: String) {
val c64KeyCode = when (command) {
"1" -> 0x31 // C64 '1'
"2" -> 0x32 // C64 '2'
"3" -> 0x33 // C64 '3'
"Y" -> 0x59 // C64 'Y'
"N" -> 0x4E // C64 'N'
else -> return
}
// Simulates a clean hardware keypress cycle
Thread {
injectNativeKey(c64KeyCode, true)
Thread.sleep(100) // Hold down frame duration
injectNativeKey(c64KeyCode, false)
}.start()
}
}
@@ -0,0 +1,74 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import com.getpebble.android.kit.PebbleKit
import com.getpebble.android.kit.PebbleKit.PebbleDataReceiver
import com.getpebble.android.kit.util.PebbleDictionary
import java.util.UUID
class EmulatorService : Service() {
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
private val KEY_COMMAND = 1
private lateinit var emulatorEngine: C64Engine
private lateinit var watchStreamer: PebbleStreamer
private var isRunning = false
private var pebbleDataReceiver: PebbleDataReceiver? = null
override fun onCreate() {
super.onCreate()
emulatorEngine = C64Engine()
watchStreamer = PebbleStreamer(this)
// Boot standard disk path containing Schwert und Magie Part 1
val romPath = filesDir.absolutePath + "/schwert_und_magie_1.d64"
emulatorEngine.initEmulator(romPath)
setupPebbleInputReceiver()
startEmulationLoop()
}
private fun setupPebbleInputReceiver() {
pebbleDataReceiver = object : PebbleDataReceiver(PEBBLE_APP_UUID) {
override fun receiveData(context: Context?, transactionId: Int, data: PebbleDictionary) {
// Read ASCII string parsed by watch selections
val command = data.getString(KEY_COMMAND)
if (command != null) {
emulatorEngine.handleWatchInput(command)
}
}
}
PebbleKit.registerReceivedDataHandler(this, pebbleDataReceiver)
}
private fun startEmulationLoop() {
isRunning = true
Thread {
val TargetFrameTimeMs = 33 // Aiming roughly around standard video refresh increments
while (isRunning) {
val startTime = System.currentTimeMillis()
emulatorEngine.runFrame()
watchStreamer.processAndStreamFrame(emulatorEngine)
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < TargetFrameTimeMs) {
Thread.sleep(TargetFrameTimeMs - elapsed)
}
}
}.start()
}
override fun onDestroy() {
isRunning = false
super.onDestroy()
try {
unregisterReceiver(pebbleDataReceiver)
} catch (e: Exception) { /* Context safety cleanup */ }
}
override fun onBind(intent: Intent?): IBinder? = null
}
@@ -0,0 +1,100 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import fi.iki.elonen.NanoHTTPD
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private const val PORT = 8888
private const val TAG = "SuM"
class MainActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
private lateinit var tvTime: TextView
private lateinit var tvLog: TextView
private lateinit var scrollLog: ScrollView
private val mainHandler = Handler(Looper.getMainLooper())
private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private var server: CompanionServer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvStatus = findViewById(R.id.tv_status)
tvTime = findViewById(R.id.tv_time)
tvLog = findViewById(R.id.tv_log)
scrollLog = findViewById(R.id.scroll_log)
server = CompanionServer(PORT, timeFormat,
onTimeFetched = { time ->
mainHandler.post { tvTime.text = "Serving time: $time" }
},
onKey = { cmd ->
Log.d(TAG, "Key received: $cmd")
mainHandler.post { appendLog(cmd) }
}
)
try {
server?.start()
tvStatus.text = "Listening on localhost:$PORT"
Log.d(TAG, "HTTP server started on port $PORT")
} catch (e: Exception) {
tvStatus.text = "Server error: ${e.message}"
Log.e(TAG, "Failed to start server", e)
}
}
private fun appendLog(entry: String) {
val timestamp = timeFormat.format(Date())
tvLog.text = "[$timestamp] $entry\n${tvLog.text}"
scrollLog.post { scrollLog.scrollTo(0, 0) }
}
override fun onDestroy() {
super.onDestroy()
server?.stop()
}
}
class CompanionServer(
port: Int,
private val timeFormat: SimpleDateFormat,
private val onTimeFetched: (String) -> Unit,
private val onKey: (String) -> Unit
) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession): Response {
Log.d(TAG, "HTTP ${session.method} ${session.uri} params=${session.parameters}")
val response = when (session.uri) {
"/time" -> {
val time = timeFormat.format(Date())
onTimeFetched(time)
newFixedLengthResponse(
Response.Status.OK,
"application/json",
"""{"time":"$time"}"""
)
}
"/key" -> {
val cmd = session.parameters["cmd"]?.firstOrNull().orEmpty()
if (cmd.isNotEmpty()) onKey(cmd)
newFixedLengthResponse(Response.Status.OK, "text/plain", "ok")
}
else -> newFixedLengthResponse(
Response.Status.NOT_FOUND, "text/plain", "not found"
)
}
response.addHeader("Access-Control-Allow-Origin", "*")
return response
}
}
@@ -0,0 +1,76 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.content.Context
import android.graphics.Bitmap
import com.getpebble.android.kit.PebbleKit
import com.getpebble.android.kit.util.PebbleDictionary
import java.nio.ByteBuffer
import java.util.UUID
class PebbleStreamer(private val context: Context) {
// Matches the unique watch app configuration ID
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
private val KEY_FRAMEBUFFER = 0
private val c64Width = 320
private val c64Height = 200
private val targetWidth = 144
private val targetHeight = 168
private val rawVideoByteBuffer = ByteBuffer.allocateDirect(c64Width * c64Height * 4)
private val previousPackedFrame = ByteArray(3024) // Base baseline to identify delta frames
fun processAndStreamFrame(engine: C64Engine) {
rawVideoByteBuffer.clear()
engine.getRawVideoBuffer(rawVideoByteBuffer)
rawVideoByteBuffer.rewind()
// 1. Downsample raw output directly to 144x168 target matrix via threshold loop
val packedFrame = ByteArray(3024) // 144 * 168 / 8 bits
var byteIndex = 0
var bitCounter = 0
var currentByte = 0
// Calculate steps for nearest-neighbor scaling while stripping non-essential margins
val scaleX = c64Width.toFloat() / targetWidth.toFloat()
val scaleY = c64Height.toFloat() / targetHeight.toFloat()
for (y in 0 until targetHeight) {
val sourceY = (y * scaleY).toInt().coerceIn(0, c64Height - 1)
for (x in 0 until targetWidth) {
val sourceX = (x * scaleX).toInt().coerceIn(0, c64Width - 1)
// Read Pixel Integer (ARGB)
val pixelIdx = (sourceY * c64Width + sourceX) * 4
val r = rawVideoByteBuffer.get(pixelIdx + 1).toInt() and 0xFF
val g = rawVideoByteBuffer.get(pixelIdx + 2).toInt() and 0xFF
val b = rawVideoByteBuffer.get(pixelIdx + 3).toInt() and 0xFF
// Traditional relative luminance formula for 1-bit thresholding
val luminance = (0.299 * r + 0.587 * g + 0.114 * b)
val bit = if (luminance > 128) 1 else 0 // 1 = White background, 0 = Dark Text
// Pack bits securely into destination byte indices
currentByte = (currentByte shl 1) or bit
bitCounter++
if (bitCounter == 8) {
packedFrame[byteIndex++] = currentByte.toByte()
currentByte = 0
bitCounter = 0
}
}
}
// 2. Perform simple Dirty Frame Check to preserve BLE airwaves
if (packedFrame.contentEquals(previousPackedFrame)) {
return // Game screen is currently static; abort transfer
}
System.arraycopy(packedFrame, 0, previousPackedFrame, 0, packedFrame.size)
// 3. Dispatch the payload out to the connected Pebble Watch
val dict = PebbleDictionary()
dict.addBytes(KEY_FRAMEBUFFER, packedFrame)
PebbleKit.sendDataToPebble(context, PEBBLE_APP_UUID, dict)
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Watch: Not connected"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Sending: --:--:--"
android:textSize="24sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Keystrokes from watch:"
android:textSize="14sp"
android:textStyle="bold" />
<ScrollView
android:id="@+id/scroll_log"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:background="#F5F5F5">
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:padding="8dp"
android:textSize="13sp" />
</ScrollView>
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">SchwertUndMagieOnPebbleCompanionApp</string>
</resources>
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
@@ -0,0 +1,15 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
@@ -0,0 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
toolchainVersion=21
@@ -0,0 +1,27 @@
[versions]
agp = "9.2.1"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activityKtx = "1.8.0"
constraintlayout = "2.1.4"
pebblekit = "4.0.1"
nanohttpd = "2.3.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
pebblekit = { group = "com.getpebble", name = "pebblekit", version.ref = "pebblekit" }
nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -0,0 +1,9 @@
#Sun May 31 17:04:53 CEST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,27 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://oss.sonatype.org/content/repositories/releases/") }
}
}
rootProject.name = "SchwertUndMagieOnPebbleCompanionApp"
include(":app")
+36
View File
@@ -0,0 +1,36 @@
# SchwertUndMagieOnPebbleFrontend
A Pebble watchapp/watchface written in C using the Pebble SDK.
## Building & running
```sh
pebble build # build for all targetPlatforms
pebble install --emulator emery # install on the emery emulator
pebble install --phone <ip> # install to a paired phone
```
## Target platforms
`targetPlatforms` in `package.json` controls which watches you build for. The
modern Pebble hardware is **emery** (Pebble Time 2), **gabbro** (Pebble Round
2), and **flint** (Pebble 2 Duo); the original Pebble platforms (aplite,
basalt, chalk, diorite) are included by default for backwards compatibility.
## Project layout
```
src/c/ C source for the watchapp
src/pkjs/ PebbleKit JS (phone-side) source, if any
worker_src/c/ Background worker source, if any
resources/ Images, fonts, and other bundled resources
package.json Project metadata (UUID, platforms, resources, message keys)
wscript Build rules — usually no need to edit
```
By default this project is configured as a watchapp. To make it a watchface,
set `pebble.watchapp.watchface` to `true` in `package.json`.
## Documentation
Full SDK docs, tutorials, and API reference: <https://developer.repebble.com>
@@ -0,0 +1,33 @@
{
"name": "sumop_frontend",
"author": "MakeAwesomeHappen",
"version": "1.0.0",
"keywords": ["pebble-app"],
"private": true,
"dependencies": {},
"pebble": {
"displayName": "SchwertUndMagieOnPebbleFrontend",
"uuid": "8039ba8c-f1e8-4620-838d-650fa35335b7",
"sdkVersion": "3",
"enableMultiJS": true,
"targetPlatforms": [
"aplite",
"basalt",
"chalk",
"diorite",
"emery",
"flint",
"gabbro"
],
"watchapp": {
"watchface": false
},
"messageKeys": [
"TIME",
"COMMAND"
],
"resources": {
"media": []
}
}
}
@@ -0,0 +1,99 @@
#include <pebble.h>
enum {
KEY_TIME = 0, // Inbound: current time string from companion app
KEY_COMMAND = 1 // Outbound: button name sent to companion app
};
static Window *s_main_window;
static TextLayer *s_time_layer;
static TextLayer *s_hint_layer;
static char s_time_buffer[32] = "Waiting...";
static void send_key_to_phone(const char *key_str) {
DictionaryIterator *iter;
app_message_outbox_begin(&iter);
if (iter == NULL) return;
dict_write_cstring(iter, KEY_COMMAND, key_str);
app_message_outbox_send();
}
static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
APP_LOG(APP_LOG_LEVEL_DEBUG, "inbox_received_callback fired");
Tuple *time_tuple = dict_find(iterator, KEY_TIME);
if (time_tuple) {
APP_LOG(APP_LOG_LEVEL_DEBUG, "KEY_TIME found, type=%d", (int)time_tuple->type);
} else {
APP_LOG(APP_LOG_LEVEL_WARNING, "KEY_TIME not found in message");
}
if (time_tuple && time_tuple->type == TUPLE_CSTRING) {
snprintf(s_time_buffer, sizeof(s_time_buffer), "%s", time_tuple->value->cstring);
APP_LOG(APP_LOG_LEVEL_DEBUG, "Setting time: %s", s_time_buffer);
text_layer_set_text(s_time_layer, s_time_buffer);
layer_mark_dirty(text_layer_get_layer(s_time_layer));
}
}
static void up_click_handler(ClickRecognizerRef recognizer, void *context) {
send_key_to_phone("UP");
}
static void down_click_handler(ClickRecognizerRef recognizer, void *context) {
send_key_to_phone("DOWN");
}
static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
send_key_to_phone("SELECT");
}
static void click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, up_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler);
window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
}
static void main_window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
s_time_layer = text_layer_create(GRect(0, 55, bounds.size.w, 50));
text_layer_set_text(s_time_layer, s_time_buffer);
text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_30_BLACK));
text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);
layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
s_hint_layer = text_layer_create(GRect(0, 130, bounds.size.w, 30));
text_layer_set_text(s_hint_layer, "UP / DN / SEL");
text_layer_set_font(s_hint_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14));
text_layer_set_text_alignment(s_hint_layer, GTextAlignmentCenter);
layer_add_child(window_layer, text_layer_get_layer(s_hint_layer));
}
static void main_window_unload(Window *window) {
text_layer_destroy(s_time_layer);
text_layer_destroy(s_hint_layer);
}
static void init(void) {
s_main_window = window_create();
window_set_click_config_provider(s_main_window, click_config_provider);
window_set_window_handlers(s_main_window, (WindowHandlers) {
.load = main_window_load,
.unload = main_window_unload
});
window_stack_push(s_main_window, true);
app_message_register_inbox_received(inbox_received_callback);
app_message_open(128, 64);
}
static void deinit(void) {
window_destroy(s_main_window);
}
int main(void) {
init();
app_event_loop();
deinit();
}
@@ -0,0 +1,58 @@
var SERVER = 'http://127.0.0.1:8888';
// Numeric keys matching the C enum: KEY_TIME=0, KEY_COMMAND=1
var KEY_TIME = 0;
var KEY_COMMAND = 1;
function httpGet(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (xhr.status !== 200) {
console.log('[SuM] HTTP ' + xhr.status + ' for ' + url);
}
};
xhr.onerror = function() {
console.log('[SuM] request failed: ' + url);
};
xhr.send();
}
function sendTimeToWatch() {
var xhr = new XMLHttpRequest();
xhr.open('GET', SERVER + '/time', true);
xhr.onload = function() {
console.log('[SuM] /time response: ' + xhr.status + ' ' + xhr.responseText);
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
var msg = {};
msg[KEY_TIME] = data.time;
console.log('[SuM] sendAppMessage key=' + KEY_TIME + ' value=' + data.time);
Pebble.sendAppMessage(
msg,
function() { console.log('[SuM] sendAppMessage ok'); },
function(e) { console.log('[SuM] sendAppMessage failed: ' + JSON.stringify(e)); }
);
} catch (err) {
console.log('[SuM] JSON parse error: ' + err);
}
}
};
xhr.onerror = function() {
console.log('[SuM] GET /time failed - is the companion app running?');
};
xhr.send();
}
Pebble.addEventListener('ready', function() {
console.log('[SuM] PebbleKit JS ready');
setInterval(sendTimeToWatch, 1000);
});
Pebble.addEventListener('appmessage', function(e) {
var command = e.payload[KEY_COMMAND];
if (!command) { return; }
console.log('[SuM] key: ' + command);
httpGet(SERVER + '/key?cmd=' + encodeURIComponent(command));
});
+54
View File
@@ -0,0 +1,54 @@
#
# This file is the default set of rules to compile a Pebble application.
#
# Feel free to customize this to your needs.
#
import os.path
top = '.'
out = 'build'
def options(ctx):
ctx.load('pebble_sdk')
def configure(ctx):
"""
This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures
a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your
change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first.
Universal configuration: add your change prior to calling ctx.load('pebble_sdk').
"""
ctx.load('pebble_sdk')
def build(ctx):
ctx.load('pebble_sdk')
build_worker = os.path.exists('worker_src')
binaries = []
cached_env = ctx.env
for platform in ctx.env.TARGET_PLATFORMS:
ctx.env = ctx.all_envs[platform]
ctx.set_group(ctx.env.PLATFORM_NAME)
app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf, bin_type='app')
if build_worker:
worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf})
ctx.pbl_build(source=ctx.path.ant_glob('worker_src/c/**/*.c'),
target=worker_elf,
bin_type='worker')
else:
binaries.append({'platform': platform, 'app_elf': app_elf})
ctx.env = cached_env
ctx.set_group('bundle')
ctx.pbl_bundle(binaries=binaries,
js=ctx.path.ant_glob(['src/pkjs/**/*.js',
'src/pkjs/**/*.json',
'src/common/**/*.js']),
js_entry_file='src/pkjs/index.js')
+60
View File
@@ -0,0 +1,60 @@
> C64 emulator able to run schwert and magie on a pebble using a companion app as emulator and a watch app as display output and input provider
That is an incredibly specific, beautifully nostalgic, and deeply chaotic engineering idea. Running Schwert und Magie (the classic German 8-part text RPG series by German Design Group) on a Pebble smartwatch via a Commodore 64 emulator companion-app setup is technically feasible, but it comes with distinct hurdles.
Because Schwert und Magie relies heavily on custom text characters and user choices, the architecture you outlined (Companion App doing the heavy lifting → Pebble Watch App handling the presentation and buttons) is actually the only realistic way to do this.
Here is a breakdown of how this architecture would function, the technical bottlenecks you'll face, and how to actually implement it.
The System Architecture
Because a Pebble smartwatch (even the Pebble Time) lacks the CPU juice and RAM to natively emulate a MOS 6510 processor and a VIC-II graphics chip, you have to split the workload.
[ Android/iOS Phone ] [ Pebble Smartwatch ]
+----------------------------+ +---------------------------+
| C64 Emulator Instance | | Pebble C Watch App |
| (Running Schwert & Magie) | | |
| | Bluetooth BLE | |
| Frame Buffer Downsampler | --------------->| Draws 144x168 Bitmap |
| | | |
| Input Mapper Matrix | <---------------| Sends Button Click Events|
+----------------------------+ +---------------------------+
1. The Phone Companion App (The Engine)
The Emulator: You need a lightweight, headless C64 emulator core (like Libretro's VICE core or a stripped-down Frodo instance) running inside your Android/iOS companion app.
The Content: You boot the .d64 or .t64 image of Schwert und Magie.
The Video Downscaler: The native C64 screen resolution is 320×200 pixels. A Pebble Classic/Time screen is 144×168 pixels. The companion app must capture the emulator's raw video buffer, convert it to 1-bit black and white (for Pebble Classic) or 64-color palette (for Pebble Time), downscale it, and pack it into a compressed byte array.
The Bridge: It listens for Pebble app messages via Bluetooth Low Energy (BLE) and translates them into C64 keyboard matrix presses.
2. The Pebble App (The Monitor & Keyboard)
The Display: It acts as a dumb terminal. It initializes a GBitmap, receives chunks of frame data via AppMessage, decompresses them, and calls layer_mark_dirty() to draw the game screen.
The Input Provider: Schwert und Magie is primarily a text adventure where you make menu choices via the C64 keyboard (e.g., pressing numbers or select letters). Since the Pebble only has Up, Select, and Down buttons on the right side, the Watch App needs a custom UI overlay—like a scrolling virtual keyboard or a quick-select wheel—to pass keycodes back to the phone.
C64-Wiki
Technical Bottlenecks & Fixes
1. The Screen Scaling / Text Legibility Problem
Schwert und Magie uses custom font chipsets to render its German text descriptions. Shrinking a 320×200 layout down to 144×168 means standard bilinear downscaling will turn the text into an unreadable slurry of pixels.
The Fix: Do not stream the video border. Crop the C64 frame strictly to the internal 320×200 text area. Even better, scale the image using a nearest-neighbor integer drop or create a custom parser in the companion app that reads the C64's RAM memory addresses directly to extract the ASCII text strings, transmitting pure text to the Pebble to let the watch render it natively in a readable system font.
2. Bluetooth Bandwidth (FPS Limits)
Pebble's AppMessage protocol over BLE maxes out at a practical limit of roughly 5 to 10 kilobytes per second depending on conditions. A raw 144×168 8-bit image is roughly 24 KB. You cannot stream 30 FPS video over a Pebble connection.
The Fix: Schwert und Magie is a static text RPG. The screen only updates when you make a move. The companion app should implement frame-differencing (interlacing or dirty-rectangles)—only sending packets when pixels actually change. When a text block finishes printing, bandwidth consumption drops to zero.
C64-Wiki
3. Input Mapping
How do you press "E" to enter a room or "1" to choose an option with only 3 buttons?
The Fix: Build a contextual menu overlay into the Pebble app. Pressing the 'Select' button pauses the video stream and opens a native Pebble menu loop (1, 2, 3, 4, Y, N, Inventory). Selecting an item fires that specific keypress back to the phone's emulator.
How to Build It (Getting Started)
If you are going to hack this together using the modern modern Rebble development ecosystem:
Pebble Watch App (C language): Use the Pebble SDK to set up an AppMessage inbox handler. Use gbitmap_create_with_data() to draw incoming byte buffers directly to the screen.
Companion App (Java/Kotlin for Android or Swift for iOS): Take an open-source Android C64 emulator frontend. Hook into the emulation loop's "Frame Ready" callback.
Data Compression: Use a lightweight compression algorithm like Run-Length Encoding (RLE) on the phone before sending the frame data. The Pebble CPU can easily decompress RLE on the fly to draw the bitmap.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
Info:
https://www.c64-wiki.de/wiki/Schwert_und_Magie
Die 8-teilige Abenteuer- und Rollenspiel-Reihe Schwert und Magie der German Design Group erschien in den Jahren 1989 bis 1992 u.a. beim boeder-Verlag. Es wurden jeweils zwei Folgen bzw. Kapitel pro Diskette gemeinsam veröffentlicht:
Schwert und Magie I: Folge 1+2 (Folge Nr. 1: Das geheimnisvolle Kraut + Folge Nr. 2: Der unheimliche Tempel)
Schwert und Magie II: Folge 3+4 (Folge Nr. 3: Das Piratenhaus + Folge Nr. 4: Die Burg des Magiers)
Schwert und Magie III: Folge 5+6 (Folge Nr. 5: Das Haus des Vampirs + Folge Nr. 6: Der Turm des Todes)
Schwert und Magie IV: Folge 7+8 (Folge Nr. 7: Unter Wasser + Folge Nr. 8: Insel der Wunder)