Auto dark/bright follows the sun at the tablet's own location
Without a light sensor an app may read (the Redmi Pad SE has none), auto is dark while the sun is more than 4° below the horizon (lib/theme/sun.dart), at Android's coarse location, polled and kept across restarts. The tablet's own, not the Orin's GNSS: the tablet has its own GNSS and cellular. Android's dark theme is the last fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -42,7 +42,7 @@ pirate-station-aide's (main views, then the rarely used ones grouped as Tools).
|
||||
`onFill(colour)` (text on a fill). No widget names a colour. Themes are JSON in `assets/themes/`, `rei` by
|
||||
default; smart-serow's four-key files still load (their `highlight` is `alarm`).
|
||||
- **Dark or bright.** `Themes.mode` is `auto` (a light sensor where the device allows one, `theme/light.dart`;
|
||||
else Android's dark theme, schedule included), `dark` or
|
||||
else the sun at the tablet's own location, `theme/sun.dart`; else Android's dark theme), `dark` or
|
||||
`bright`, chosen in Settings and kept across restarts. The status bar's button holds the other variant;
|
||||
a long press there is auto again.
|
||||
- **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own
|
||||
|
||||
+12
-9
@@ -1,16 +1,18 @@
|
||||
// Yukart display: the tablet app. Starts the link to the Orin and hands the screen to
|
||||
// the shell (lib/shell.dart). Code layout: lib/README.md.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'link/boundary.dart';
|
||||
import 'link/net.dart';
|
||||
import 'link/reading.dart';
|
||||
import 'pages/link_test.dart';
|
||||
import 'shell.dart';
|
||||
import 'shell_scope.dart';
|
||||
import 'theme/light.dart';
|
||||
import 'theme/palette.dart';
|
||||
import 'theme/sun.dart';
|
||||
|
||||
/// Override for development, e.g. `--dart-define=YUKART_GW=ws://halogen.local:7700/`.
|
||||
const defaultUrl = String.fromEnvironment(
|
||||
@@ -25,14 +27,15 @@ Future<void> main() async {
|
||||
final usb = UsbLink();
|
||||
await usb.bindFor(Uri.parse(defaultUrl));
|
||||
final boundary = Boundary(defaultUrl)..start();
|
||||
// The sun, for auto dark/bright: the car's position while GNSS has one.
|
||||
boundary.addListener(() {
|
||||
final lat = Reading.of(boundary, 'gnss_lat');
|
||||
final lon = Reading.of(boundary, 'gnss_lon');
|
||||
if (!lat.dim && !lon.dim && lat.number != null && lon.number != null) {
|
||||
themes.setPosition(lat.number!, lon.number!);
|
||||
}
|
||||
});
|
||||
// The sun, for auto dark/bright: the tablet's own location, every minute until there
|
||||
// is one (the first ask brings up the permission), then every ten.
|
||||
Future<void> locate() async {
|
||||
final p = await androidLocation();
|
||||
if (p != null) themes.setPosition(p.$1, p.$2);
|
||||
Timer(Duration(minutes: p == null ? 1 : 10), locate);
|
||||
}
|
||||
|
||||
locate();
|
||||
final linkTest = LinkTest(usb);
|
||||
// Development hook: `am start ... --es linktest [dl:|bi:]<label>[@<host>]` runs the test
|
||||
// (`dl:` the download series, `bi:` burst latency against idle gaps), on the gateway's host unless one is given.
|
||||
|
||||
@@ -65,9 +65,9 @@ class SettingsPage extends StatelessWidget {
|
||||
? 'now ${lux.toStringAsFixed(lux < 10 ? 1 : 0)} lux; dark at '
|
||||
'${LightSwitch.darkAt.round()} or less, bright from ${LightSwitch.brightAt.round()}'
|
||||
: sun != null
|
||||
? 'now ${sun.round()}° above the horizon at the last GNSS position; '
|
||||
'dark below ${darkBelowDeg.round()}°'
|
||||
: 'no GNSS position yet';
|
||||
? 'the sun is ${sun.abs().round()}° ${sun < 0 ? 'below' : 'above'} the horizon here; '
|
||||
'dark from ${(-darkBelowDeg).round()}° below'
|
||||
: 'no location yet';
|
||||
return Text(
|
||||
'Auto follows ${themes.autoSource} ($now). '
|
||||
'The status bar button holds one; a long press there is auto again.',
|
||||
|
||||
@@ -127,7 +127,7 @@ enum VariantMode { auto, dark, bright }
|
||||
|
||||
/// A theme: both variants, and which one is showing. Listen to it to follow a switch.
|
||||
/// In auto it follows a light sensor ([followLight]) where the device lets an app read
|
||||
/// one; else the sun at the car's last position ([setPosition]); else Android's dark
|
||||
/// one; else the sun at the tablet's last location ([setPosition]); else Android's dark
|
||||
/// theme (by hand or on its schedule). The mode and the position are kept across restarts.
|
||||
class Themes extends ChangeNotifier with WidgetsBindingObserver {
|
||||
Themes(
|
||||
@@ -210,7 +210,7 @@ class Themes extends ChangeNotifier with WidgetsBindingObserver {
|
||||
bool? _sunDark;
|
||||
late final Timer _sunTimer;
|
||||
|
||||
/// The car's position, for the sun. Kept (to 0.1°, about 10 km: plenty for the sun)
|
||||
/// The tablet's location, for the sun. Kept (to 0.1°, about 10 km: plenty for the sun)
|
||||
/// so auto knows the sun before the first fix.
|
||||
void setPosition(double lat, double lon) {
|
||||
double r(double x) => (x * 10).round() / 10;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Auto dark/bright from the sun: dark once it is far enough below the horizon, at the
|
||||
// tablet's own rough location (Android's, not the Orin's: the tablet stands on its own).
|
||||
// Google Maps on this tablet does the same (it reads no light sensor; checked). Needs no
|
||||
// sensor; does not know about tunnels or car parks.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// The tablet's newest known location, roughly, from MainActivity; null without one
|
||||
/// (or before location is granted: the first call asks).
|
||||
Future<(double, double)?> androidLocation() async {
|
||||
try {
|
||||
final v = await const MethodChannel('yukart/location')
|
||||
.invokeListMethod<double>('last');
|
||||
return v == null || v.length < 2 ? null : (v[0], v[1]);
|
||||
} catch (_) {
|
||||
return null; // No Android side (tests, desktop).
|
||||
}
|
||||
}
|
||||
|
||||
/// Dark while the sun is lower than this (degrees; sunset is -0.8, civil dusk -6).
|
||||
const darkBelowDeg = -4.0;
|
||||
|
||||
/// The sun's elevation above the horizon, in degrees, at [lat], [lon] (degrees, east
|
||||
/// positive) and [time]. The usual low-precision formulae (the Astronomical Almanac's,
|
||||
/// as NOAA uses them): within about a degree, plenty for this.
|
||||
double sunElevation(double lat, double lon, DateTime time) {
|
||||
double rad(double d) => d * math.pi / 180;
|
||||
double deg(double r) => r * 180 / math.pi;
|
||||
final d =
|
||||
time.toUtc().millisecondsSinceEpoch / 86400000 + 2440587.5 - 2451545.0;
|
||||
final g = rad(357.529 + 0.98560028 * d); // mean anomaly
|
||||
final q = 280.459 + 0.98564736 * d; // mean longitude
|
||||
final l = rad(q + 1.915 * math.sin(g) + 0.020 * math.sin(2 * g));
|
||||
final e = rad(23.439 - 0.00000036 * d); // obliquity
|
||||
final ra = math.atan2(math.cos(e) * math.sin(l), math.cos(l));
|
||||
final dec = math.asin(math.sin(e) * math.sin(l));
|
||||
final gmst = (18.697374558 + 24.06570982441908 * d) % 24;
|
||||
final ha = rad(gmst * 15 + lon) - ra;
|
||||
final phi = rad(lat);
|
||||
return deg(
|
||||
math.asin(
|
||||
math.sin(phi) * math.sin(dec) +
|
||||
math.cos(phi) * math.cos(dec) * math.cos(ha),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user