Removed dead code
This commit is contained in:
@@ -42,7 +42,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.nanohttpd)
|
implementation(libs.nanohttpd)
|
||||||
implementation(libs.pebblekit)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ appcompat = "1.6.1"
|
|||||||
material = "1.10.0"
|
material = "1.10.0"
|
||||||
activityKtx = "1.8.0"
|
activityKtx = "1.8.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
pebblekit = "4.0.1"
|
|
||||||
nanohttpd = "2.3.1"
|
nanohttpd = "2.3.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -20,7 +19,6 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
|||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
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" }
|
nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri("https://oss.sonatype.org/content/repositories/releases/") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,16 +20,9 @@ static void send_key_to_phone(const char *key_str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
|
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);
|
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) {
|
if (time_tuple && time_tuple->type == TUPLE_CSTRING) {
|
||||||
snprintf(s_time_buffer, sizeof(s_time_buffer), "%s", time_tuple->value->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);
|
text_layer_set_text(s_time_layer, s_time_buffer);
|
||||||
layer_mark_dirty(text_layer_get_layer(s_time_layer));
|
layer_mark_dirty(text_layer_get_layer(s_time_layer));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
var SERVER = 'http://127.0.0.1:8888';
|
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_TIME = 0;
|
||||||
var KEY_COMMAND = 1;
|
var KEY_COMMAND = 1;
|
||||||
|
|
||||||
function httpGet(url) {
|
function httpGet(url) {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', url, true);
|
xhr.open('GET', url, true);
|
||||||
xhr.onload = function() {
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
console.log('[SuM] HTTP ' + xhr.status + ' for ' + url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = function() {
|
xhr.onerror = function() {
|
||||||
console.log('[SuM] request failed: ' + url);
|
console.log('[SuM] request failed: ' + url);
|
||||||
};
|
};
|
||||||
@@ -22,16 +16,14 @@ function sendTimeToWatch() {
|
|||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', SERVER + '/time', true);
|
xhr.open('GET', SERVER + '/time', true);
|
||||||
xhr.onload = function() {
|
xhr.onload = function() {
|
||||||
console.log('[SuM] /time response: ' + xhr.status + ' ' + xhr.responseText);
|
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
try {
|
try {
|
||||||
var data = JSON.parse(xhr.responseText);
|
var data = JSON.parse(xhr.responseText);
|
||||||
var msg = {};
|
var msg = {};
|
||||||
msg[KEY_TIME] = data.time;
|
msg[KEY_TIME] = data.time;
|
||||||
console.log('[SuM] sendAppMessage key=' + KEY_TIME + ' value=' + data.time);
|
|
||||||
Pebble.sendAppMessage(
|
Pebble.sendAppMessage(
|
||||||
msg,
|
msg,
|
||||||
function() { console.log('[SuM] sendAppMessage ok'); },
|
function() {},
|
||||||
function(e) { console.log('[SuM] sendAppMessage failed: ' + JSON.stringify(e)); }
|
function(e) { console.log('[SuM] sendAppMessage failed: ' + JSON.stringify(e)); }
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -46,13 +38,11 @@ function sendTimeToWatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pebble.addEventListener('ready', function() {
|
Pebble.addEventListener('ready', function() {
|
||||||
console.log('[SuM] PebbleKit JS ready');
|
|
||||||
setInterval(sendTimeToWatch, 1000);
|
setInterval(sendTimeToWatch, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
Pebble.addEventListener('appmessage', function(e) {
|
Pebble.addEventListener('appmessage', function(e) {
|
||||||
var command = e.payload[KEY_COMMAND];
|
var command = e.payload[KEY_COMMAND];
|
||||||
if (!command) { return; }
|
if (!command) { return; }
|
||||||
console.log('[SuM] key: ' + command);
|
|
||||||
httpGet(SERVER + '/key?cmd=' + encodeURIComponent(command));
|
httpGet(SERVER + '/key?cmd=' + encodeURIComponent(command));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user