Initial commit, adding original binaries, initial watch and companion apps which can talk
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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
|
||||
@@ -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" "$@"
|
||||
@@ -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")
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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')
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||