Initial commit: Android & Garmin Remote Camera App with Live Preview
This commit is contained in:
15
garmin-app/manifest.xml
Normal file
15
garmin-app/manifest.xml
Normal 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
1
garmin-app/monkey.jungle
Normal file
@@ -0,0 +1 @@
|
||||
project.manifest = manifest.xml
|
||||
3
garmin-app/resources/drawables/drawables.xml
Normal file
3
garmin-app/resources/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png" />
|
||||
</resources>
|
||||
BIN
garmin-app/resources/drawables/launcher_icon.png
Normal file
BIN
garmin-app/resources/drawables/launcher_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 675 B |
4
garmin-app/resources/strings/strings.xml
Normal file
4
garmin-app/resources/strings/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string id="AppName">Foto Companion</string>
|
||||
<string id="PressToCapture">Press Start</string>
|
||||
</resources>
|
||||
31
garmin-app/source/FotoCompanionApp.mc
Normal file
31
garmin-app/source/FotoCompanionApp.mc
Normal 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>;
|
||||
}
|
||||
}
|
||||
39
garmin-app/source/FotoCompanionDelegate.mc
Normal file
39
garmin-app/source/FotoCompanionDelegate.mc
Normal 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");
|
||||
}
|
||||
}
|
||||
101
garmin-app/source/FotoCompanionView.mc
Normal file
101
garmin-app/source/FotoCompanionView.mc
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user