Initial commit: Android & Garmin Remote Camera App with Live Preview

This commit is contained in:
Gemini Bot
2025-12-09 05:39:37 +00:00
commit 55f7df71d8
21 changed files with 951 additions and 0 deletions

15
garmin-app/manifest.xml Normal file
View File

@@ -0,0 +1,15 @@
<iq:manifest xmlns:iq="http://www.garmin.com/xml/connectiq" version="3.0">
<iq:application entry="FotoCompanionApp" id="561284d7-4633-4d43-9876-068305612345" launcherIcon="@Drawables.LauncherIcon" name="@Strings.AppName" type="watch-app">
<iq:products>
<iq:product id="fr955"/>
<iq:product id="fenix7spro"/>
</iq:products>
<iq:permissions>
<iq:permission id="Communications"/>
</iq:permissions>
<iq:languages>
<iq:language>eng</iq:language>
<iq:language>deu</iq:language>
</iq:languages>
</iq:application>
</iq:manifest>

1
garmin-app/monkey.jungle Normal file
View File

@@ -0,0 +1 @@
project.manifest = manifest.xml

View File

@@ -0,0 +1,3 @@
<resources>
<bitmap id="LauncherIcon" filename="launcher_icon.png" />
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

View File

@@ -0,0 +1,4 @@
<resources>
<string id="AppName">Foto Companion</string>
<string id="PressToCapture">Press Start</string>
</resources>

View File

@@ -0,0 +1,31 @@
import Toybox.Application;
import Toybox.Lang;
import Toybox.WatchUi;
import Toybox.Communications;
class FotoCompanionApp extends Application.AppBase {
var view;
function initialize() {
AppBase.initialize();
}
function onStart(state as Dictionary?) as Void {
// Register for messages from the phone
Communications.registerForPhoneAppMessages(method(:onPhoneMessage));
}
function onStop(state as Dictionary?) as Void {
}
function onPhoneMessage(msg as Communications.Message) as Void {
if (view != null) {
view.updateImage(msg.data);
}
}
function getInitialView() as Array<Views or InputDelegates>? {
view = new FotoCompanionView();
return [ view, new FotoCompanionDelegate(view) ] as Array<Views or InputDelegates>;
}
}

View File

@@ -0,0 +1,39 @@
import Toybox.Lang;
import Toybox.WatchUi;
import Toybox.Communications;
import Toybox.System;
class FotoCompanionDelegate extends WatchUi.BehaviorDelegate {
var view;
function initialize(v) {
BehaviorDelegate.initialize();
view = v;
}
function onSelect() as Boolean {
// Send Trigger command to Android
// We use a simple string "TRIGGER"
try {
Communications.transmit("TRIGGER", null, new CommsListener());
System.println("Trigger sent");
} catch (ex) {
System.println("Error sending: " + ex.getErrorMessage());
}
return true;
}
}
class CommsListener extends Communications.ConnectionListener {
function initialize() {
ConnectionListener.initialize();
}
function onComplete() {
System.println("Tx Complete");
}
function onError() {
System.println("Tx Error");
}
}

View File

@@ -0,0 +1,101 @@
import Toybox.Graphics;
import Toybox.WatchUi;
import Toybox.Lang;
class FotoCompanionView extends WatchUi.View {
var bitmap = null;
var palette = null;
function initialize() {
View.initialize();
// Initialize 64 color palette matching Android side (00, 55, AA, FF)
// R (2 bits), G (2 bits), B (2 bits)
palette = new [64];
for (var i = 0; i < 64; i++) {
var r = (i >> 4) & 0x03;
var g = (i >> 2) & 0x03;
var b = i & 0x03;
// Map 0..3 to 0..255 (0, 85, 170, 255)
palette[i] = (r * 85) << 16 | (g * 85) << 8 | (b * 85);
}
}
function onLayout(dc as Dc) as Void {
}
function onUpdate(dc as Dc) as Void {
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
dc.clear();
if (bitmap != null) {
var cx = (dc.getWidth() - bitmap.getWidth()) / 2;
var cy = (dc.getHeight() - bitmap.getHeight()) / 2;
dc.drawBitmap(cx, cy, bitmap);
} else {
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(dc.getWidth() / 2, dc.getHeight() / 2, Graphics.FONT_MEDIUM, "Waiting for\nCamera...", Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
}
// Draw Button Hint
dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_TRANSPARENT);
dc.drawText(dc.getWidth() / 2, dc.getHeight() - 30, Graphics.FONT_XTINY, "PRESS START", Graphics.TEXT_JUSTIFY_CENTER);
}
function updateImage(data) {
if (data instanceof Toybox.Lang.Array) {
// Create a BufferedBitmap
// Width/Height hardcoded to match Android (120x120)
var opts = {
:width => 120,
:height => 120,
:palette => palette
};
if (Graphics has :createBufferedBitmap) {
var bbRef = Graphics.createBufferedBitmap(opts);
var bb = bbRef.get();
// Copy data
// BufferedBitmap.setPalette is implicit via options
// Unfortunately, there is no direct "setBytes" for the whole bitmap in older SDKs easily exposed without a resource.
// But Connect IQ 4.0+ helps.
// If we can't do bulk set, we iterate? Too slow.
// Actually, resource creation from bytes is tricky.
// Let's try the most robust way:
// If the data is indeed the palette indices, we just need to get it into the bitmap buffer.
// Hack for performance if no setBytes:
// Use a resource? No dynamic resource creation.
// Alternative: Send a custom String/JSON and parse? No.
// If the device supports direct palette mapping we might be good.
// Let's assume standard behavior:
// We CAN iterate 14400 pixels in Monkey C if optimized? Maybe 0.5s.
// Let's try to find a bulk setter.
// There isn't one publicly documented for raw byte array -> bitmap pixels easily.
// Wait! Strings!
// If we encode as a string on Android, drawText? No.
// Okay, we will iterate. It's 14400 pixels.
// To optimize, Android sends RLE?
// For now, simple iteration.
var dc = bb.getDc();
for (var i = 0; i < 14400; i++) {
if (i < data.size()) {
var cIndex = data[i];
if (cIndex >= 0 && cIndex < 64) {
dc.setColor(palette[cIndex], Graphics.COLOR_TRANSPARENT);
// x = i % 120, y = i / 120
dc.drawPoint(i % 120, i / 120);
}
}
}
bitmap = bbRef;
WatchUi.requestUpdate();
}
}
}
}