Phase 1: foundation modules
Central Config with all tunables (retention, GPS timeout, colors, event type keys, menu item metadata). Event / EventStore handle persistence via Application.Storage with 7-day pruning. Logger mirrors the same retention. LayoutMetrics and Haversine provide resolution-independent geometry helpers. German UI strings and placeholder menu icons landed alongside so the build stays green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6f56c337f7
commit
43f970d764
19 changed files with 409 additions and 2 deletions
62
source/Config.mc
Normal file
62
source/Config.mc
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
|
||||
// Central configuration. All tunable values live here so they can be
|
||||
// changed without touching feature code.
|
||||
module Config {
|
||||
|
||||
// --- Retention ------------------------------------------------------
|
||||
const RETENTION_SEC = 7 * 24 * 60 * 60;
|
||||
|
||||
// --- GPS ------------------------------------------------------------
|
||||
const GPS_TIMEOUT_MS = 10000;
|
||||
const GPS_TARGET_ACCURACY_M = 5;
|
||||
|
||||
// --- Animation / UI timings ----------------------------------------
|
||||
const SUCCESS_DISPLAY_MS = 2500;
|
||||
const ERROR_DISPLAY_MS = 5000;
|
||||
const DELETE_HOLD_MS = 2500;
|
||||
|
||||
// --- Address resolution --------------------------------------------
|
||||
const ADDRESS_MAX_DISTANCE_M = 20;
|
||||
const PHOTON_URL = "https://photon.komoot.io/reverse";
|
||||
|
||||
// --- Colors (AMOLED: black background, white fg) -------------------
|
||||
const COLOR_BG = 0x000000;
|
||||
const COLOR_FG = 0xFFFFFF;
|
||||
const COLOR_SUCCESS = 0x00AA00;
|
||||
const COLOR_ERROR = 0xFF2222;
|
||||
const COLOR_DELETE = 0xFF2222;
|
||||
const COLOR_ACCENT = 0xFFFFFF;
|
||||
|
||||
// --- Event type keys ------------------------------------------------
|
||||
const EVENT_GENERAL = "general";
|
||||
const EVENT_START = "start";
|
||||
const EVENT_END = "end";
|
||||
const EVENT_ARRIVAL = "arrival";
|
||||
const EVENT_ARREST = "arrest";
|
||||
const EVENT_FORCE = "force";
|
||||
const EVENT_EVIDENCE = "evidence";
|
||||
const EVENT_SIGHTING = "sighting";
|
||||
|
||||
// --- Special menu actions ------------------------------------------
|
||||
const ACTION_HISTORY = "history";
|
||||
const ACTION_DELETE = "delete";
|
||||
|
||||
// Menu item metadata. Order matches display order (index 0 is first
|
||||
// selected). Each entry: key, drawable id, string id, highlight color.
|
||||
function menuItems() as Array<Dictionary> {
|
||||
return [
|
||||
{ :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :label => Rez.Strings.menu_history, :color => 0xFFFFFF },
|
||||
{ :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :label => Rez.Strings.menu_general, :color => 0xFFAA00 },
|
||||
{ :key => EVENT_START, :icon => Rez.Drawables.IconStart, :label => Rez.Strings.menu_start, :color => 0x00FF00 },
|
||||
{ :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :label => Rez.Strings.menu_end, :color => 0x00AAFF },
|
||||
{ :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :label => Rez.Strings.menu_arrival, :color => 0xFFFF00 },
|
||||
{ :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :label => Rez.Strings.menu_arrest, :color => 0xFF0088 },
|
||||
{ :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :label => Rez.Strings.menu_force, :color => 0xAA00FF },
|
||||
{ :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :label => Rez.Strings.menu_evidence, :color => 0x00FFFF },
|
||||
{ :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :label => Rez.Strings.menu_sighting, :color => 0xFF8800 },
|
||||
{ :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :label => Rez.Strings.menu_delete, :color => 0xFF2222 }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,10 @@ class EinsatzprotokollApp extends Application.AppBase {
|
|||
AppBase.initialize();
|
||||
}
|
||||
|
||||
function onStart(state as Dictionary?) as Void {}
|
||||
function onStart(state as Dictionary?) as Void {
|
||||
EventStore.pruneOld();
|
||||
Logger.pruneOld();
|
||||
}
|
||||
|
||||
function onStop(state as Dictionary?) as Void {}
|
||||
|
||||
|
|
|
|||
41
source/Event.mc
Normal file
41
source/Event.mc
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import Toybox.Lang;
|
||||
|
||||
// Value object for a protocol entry. Serialized as a plain Dictionary
|
||||
// so Application.Storage can persist it.
|
||||
class Event {
|
||||
public var type as String;
|
||||
public var timestamp as Number; // unix seconds
|
||||
public var lat as Float or Null;
|
||||
public var lon as Float or Null;
|
||||
public var accuracy as Float or Null;
|
||||
|
||||
function initialize(type as String, timestamp as Number,
|
||||
lat as Float or Null, lon as Float or Null,
|
||||
accuracy as Float or Null) {
|
||||
self.type = type;
|
||||
self.timestamp = timestamp;
|
||||
self.lat = lat;
|
||||
self.lon = lon;
|
||||
self.accuracy = accuracy;
|
||||
}
|
||||
|
||||
function toDict() as Dictionary {
|
||||
return {
|
||||
"t" => type,
|
||||
"ts" => timestamp,
|
||||
"lat" => lat,
|
||||
"lon" => lon,
|
||||
"acc" => accuracy
|
||||
};
|
||||
}
|
||||
|
||||
static function fromDict(d as Dictionary) as Event {
|
||||
return new Event(
|
||||
d["t"] as String,
|
||||
d["ts"] as Number,
|
||||
d["lat"] as Float or Null,
|
||||
d["lon"] as Float or Null,
|
||||
d["acc"] as Float or Null
|
||||
);
|
||||
}
|
||||
}
|
||||
73
source/EventStore.mc
Normal file
73
source/EventStore.mc
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import Toybox.Application;
|
||||
import Toybox.Lang;
|
||||
import Toybox.Time;
|
||||
|
||||
// Persists events in Application.Storage as an array of dictionaries,
|
||||
// ordered oldest → newest. Prunes entries older than Config.RETENTION_SEC
|
||||
// whenever loaded.
|
||||
module EventStore {
|
||||
|
||||
const KEY = "events";
|
||||
|
||||
function getAll() as Array<Event> {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
if (raw == null || !(raw instanceof Array)) {
|
||||
return [];
|
||||
}
|
||||
var out = [];
|
||||
for (var i = 0; i < raw.size(); i++) {
|
||||
out.add(Event.fromDict(raw[i] as Dictionary));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function add(event as Event) as Void {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
var arr = (raw instanceof Array) ? raw : [];
|
||||
arr.add(event.toDict());
|
||||
Application.Storage.setValue(KEY, arr);
|
||||
}
|
||||
|
||||
function deleteLast() as Boolean {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
if (!(raw instanceof Array) || raw.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
raw = raw.slice(0, raw.size() - 1);
|
||||
Application.Storage.setValue(KEY, raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
function count() as Number {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
return (raw instanceof Array) ? raw.size() : 0;
|
||||
}
|
||||
|
||||
function latest() as Event or Null {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
if (!(raw instanceof Array) || raw.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return Event.fromDict(raw[raw.size() - 1] as Dictionary);
|
||||
}
|
||||
|
||||
// Drops events with timestamp < (now - retention).
|
||||
function pruneOld() as Void {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
if (!(raw instanceof Array) || raw.size() == 0) {
|
||||
return;
|
||||
}
|
||||
var cutoff = Time.now().value() - Config.RETENTION_SEC;
|
||||
var kept = [];
|
||||
for (var i = 0; i < raw.size(); i++) {
|
||||
var d = raw[i] as Dictionary;
|
||||
var ts = d["ts"];
|
||||
if (ts != null && (ts as Number) >= cutoff) {
|
||||
kept.add(d);
|
||||
}
|
||||
}
|
||||
if (kept.size() != raw.size()) {
|
||||
Application.Storage.setValue(KEY, kept);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
source/Haversine.mc
Normal file
25
source/Haversine.mc
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Toybox.Lang;
|
||||
import Toybox.Math;
|
||||
|
||||
// Great-circle distance between two WGS84 coordinates, in meters.
|
||||
module Haversine {
|
||||
|
||||
const EARTH_RADIUS_M = 6371000.0;
|
||||
|
||||
function distance(lat1 as Float, lon1 as Float, lat2 as Float, lon2 as Float) as Float {
|
||||
var dLat = _toRad(lat2 - lat1);
|
||||
var dLon = _toRad(lon2 - lon1);
|
||||
var rLat1 = _toRad(lat1);
|
||||
var rLat2 = _toRad(lat2);
|
||||
|
||||
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
|
||||
+ Math.cos(rLat1) * Math.cos(rLat2)
|
||||
* Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
var c = 2 * Math.asin(Math.sqrt(a));
|
||||
return (EARTH_RADIUS_M * c).toFloat();
|
||||
}
|
||||
|
||||
function _toRad(deg as Float) as Float {
|
||||
return (deg * Math.PI / 180.0).toFloat();
|
||||
}
|
||||
}
|
||||
52
source/LayoutMetrics.mc
Normal file
52
source/LayoutMetrics.mc
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
|
||||
// Resolution-independent layout helpers. All sizes are computed as
|
||||
// fractions of the drawable context so the app adapts to any round
|
||||
// Garmin display.
|
||||
module LayoutMetrics {
|
||||
|
||||
// --- Menu ring geometry --------------------------------------------
|
||||
function centerX(dc as Dc) as Number { return dc.getWidth() / 2; }
|
||||
function centerY(dc as Dc) as Number { return dc.getHeight() / 2; }
|
||||
|
||||
// Radius on which the 10 icons sit.
|
||||
function ringRadius(dc as Dc) as Number {
|
||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
||||
return (min * 0.38).toNumber();
|
||||
}
|
||||
|
||||
function iconSize(dc as Dc) as Number {
|
||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
||||
return (min * 0.14).toNumber();
|
||||
}
|
||||
|
||||
function selectedIconSize(dc as Dc) as Number {
|
||||
return (iconSize(dc) * 1.3).toNumber();
|
||||
}
|
||||
|
||||
// --- History view sections -----------------------------------------
|
||||
function topSectionHeight(dc as Dc) as Number {
|
||||
return (dc.getHeight() * 0.15).toNumber();
|
||||
}
|
||||
|
||||
function midSectionHeight(dc as Dc) as Number {
|
||||
return (dc.getHeight() * 0.70).toNumber();
|
||||
}
|
||||
|
||||
function bottomSectionY(dc as Dc) as Number {
|
||||
return (dc.getHeight() * 0.85).toNumber();
|
||||
}
|
||||
|
||||
// --- Loading / delete arc radius ------------------------------------
|
||||
function edgeArcRadius(dc as Dc) as Number {
|
||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
||||
return (min * 0.48).toNumber();
|
||||
}
|
||||
|
||||
function edgeArcPenWidth(dc as Dc) as Number {
|
||||
var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight();
|
||||
var w = (min * 0.025).toNumber();
|
||||
return (w < 3) ? 3 : w;
|
||||
}
|
||||
}
|
||||
49
source/Logger.mc
Normal file
49
source/Logger.mc
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import Toybox.Application;
|
||||
import Toybox.Lang;
|
||||
import Toybox.Time;
|
||||
|
||||
// Lightweight crash / diagnostic logger. Entries live in
|
||||
// Application.Storage and are pruned with the same retention window
|
||||
// as events so the watch storage never fills up.
|
||||
module Logger {
|
||||
|
||||
const KEY = "logs";
|
||||
const MAX_ENTRIES = 50;
|
||||
|
||||
function log(msg as String) as Void {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
var arr = (raw instanceof Array) ? raw : [];
|
||||
arr.add({
|
||||
"ts" => Time.now().value(),
|
||||
"msg" => msg
|
||||
});
|
||||
if (arr.size() > MAX_ENTRIES) {
|
||||
arr = arr.slice(arr.size() - MAX_ENTRIES, arr.size());
|
||||
}
|
||||
Application.Storage.setValue(KEY, arr);
|
||||
}
|
||||
|
||||
function getAll() as Array<Dictionary> {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
return (raw instanceof Array) ? raw : [];
|
||||
}
|
||||
|
||||
function pruneOld() as Void {
|
||||
var raw = Application.Storage.getValue(KEY);
|
||||
if (!(raw instanceof Array) || raw.size() == 0) {
|
||||
return;
|
||||
}
|
||||
var cutoff = Time.now().value() - Config.RETENTION_SEC;
|
||||
var kept = [];
|
||||
for (var i = 0; i < raw.size(); i++) {
|
||||
var d = raw[i] as Dictionary;
|
||||
var ts = d["ts"];
|
||||
if (ts != null && (ts as Number) >= cutoff) {
|
||||
kept.add(d);
|
||||
}
|
||||
}
|
||||
if (kept.size() != raw.size()) {
|
||||
Application.Storage.setValue(KEY, kept);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue