backend deployment update and navigator shake animation
- backend: now runs uv sync at service start to make sure of uv lock status. might migrate to package/bundle - navigator now shakes when entering 'surprise' state
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -65,6 +65,8 @@ scripts/*.pyo
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
|
||||||
# extra resources
|
# extra resources
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
StatBox(value: _formatInt(_rpm), label: 'RPM'),
|
StatBox(value: _formatInt(_rpm), label: 'RPM', isWarning: () => (_rpm ?? 0) > 4000),
|
||||||
StatBox(value: _formatInt(_engineTemp), unit: '°C', label: 'ENG'),
|
StatBox(value: _formatInt(_engineTemp), unit: '°C', label: 'ENG', isWarning: () => (_engineTemp ?? 0) > 120),
|
||||||
StatBox(value: _formatGear(_gear), label: 'GEAR'),
|
StatBox(value: _formatGear(_gear), label: 'GEAR'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../services/config_service.dart';
|
import '../services/config_service.dart';
|
||||||
|
|
||||||
@@ -18,14 +19,34 @@ class NavigatorWidget extends StatefulWidget {
|
|||||||
State<NavigatorWidget> createState() => NavigatorWidgetState();
|
State<NavigatorWidget> createState() => NavigatorWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavigatorWidgetState extends State<NavigatorWidget> {
|
class NavigatorWidgetState extends State<NavigatorWidget>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
String _emotion = 'default';
|
String _emotion = 'default';
|
||||||
|
late AnimationController _shakeController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_shakeController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_shakeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the displayed emotion.
|
/// Change the displayed emotion.
|
||||||
/// Image file must exist at: {assetsPath}/navigator/{navigator}/{emotion}.png
|
/// Image file must exist at: {assetsPath}/navigator/{navigator}/{emotion}.png
|
||||||
void setEmotion(String emotion) {
|
void setEmotion(String emotion) {
|
||||||
if (emotion != _emotion) {
|
if (emotion != _emotion) {
|
||||||
setState(() => _emotion = emotion);
|
setState(() => _emotion = emotion);
|
||||||
|
if (emotion == 'surprise') {
|
||||||
|
_shakeController.forward(from: 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +61,7 @@ class NavigatorWidgetState extends State<NavigatorWidget> {
|
|||||||
final config = ConfigService.instance;
|
final config = ConfigService.instance;
|
||||||
final basePath = '${config.assetsPath}/navigator/${config.navigator}';
|
final basePath = '${config.assetsPath}/navigator/${config.navigator}';
|
||||||
|
|
||||||
return Image.file(
|
final image = Image.file(
|
||||||
File('$basePath/$_emotion.png'),
|
File('$basePath/$_emotion.png'),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
@@ -55,5 +76,19 @@ class NavigatorWidgetState extends State<NavigatorWidget> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Shake animation for surprise
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _shakeController,
|
||||||
|
child: image,
|
||||||
|
builder: (context, child) {
|
||||||
|
final shake = sin(_shakeController.value * pi * 6) * 10 *
|
||||||
|
(1 - _shakeController.value); // 6 oscillations, 4px amplitude, decay
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(shake, 0),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,23 @@ class StatBox extends StatelessWidget {
|
|||||||
final String label;
|
final String label;
|
||||||
final int flex;
|
final int flex;
|
||||||
|
|
||||||
|
/// Optional warning predicate - if returns true, value shows in highlight color
|
||||||
|
final bool Function()? isWarning;
|
||||||
|
|
||||||
const StatBox({
|
const StatBox({
|
||||||
super.key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
this.unit,
|
this.unit,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.flex = 1,
|
this.flex = 1,
|
||||||
|
this.isWarning,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = AppTheme.of(context);
|
final theme = AppTheme.of(context);
|
||||||
|
final warning = isWarning?.call() ?? false;
|
||||||
|
final valueColor = warning ? theme.highlight : theme.foreground;
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
flex: flex,
|
flex: flex,
|
||||||
@@ -42,7 +48,7 @@ class StatBox extends StatelessWidget {
|
|||||||
fontSize: baseSize,
|
fontSize: baseSize,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontFeatures: const [FontFeature.tabularFigures()],
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
color: theme.foreground,
|
color: valueColor,
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ SCRIPT_DIR = Path(__file__).parent.resolve()
|
|||||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||||
CONFIG_FILE = SCRIPT_DIR / "deploy_target.json"
|
CONFIG_FILE = SCRIPT_DIR / "deploy_target.json"
|
||||||
BACKEND_DIR = PROJECT_ROOT / "pi" / "backend"
|
BACKEND_DIR = PROJECT_ROOT / "pi" / "backend"
|
||||||
|
SERVICE_FILE = SCRIPT_DIR / "smartserow-backend.service"
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess:
|
def run(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess:
|
||||||
@@ -72,6 +73,7 @@ def deploy(restart: bool = False) -> bool:
|
|||||||
"--exclude", "*.pyc",
|
"--exclude", "*.pyc",
|
||||||
"--exclude", ".venv",
|
"--exclude", ".venv",
|
||||||
"--exclude", ".ruff_cache",
|
"--exclude", ".ruff_cache",
|
||||||
|
"--exclude", "uv.lock", # Let Pi generate its own lockfile
|
||||||
f"{BACKEND_DIR}/",
|
f"{BACKEND_DIR}/",
|
||||||
f"{ssh_target}:{remote_path}/",
|
f"{ssh_target}:{remote_path}/",
|
||||||
])
|
])
|
||||||
@@ -88,6 +90,16 @@ def deploy(restart: bool = False) -> bool:
|
|||||||
print("WARNING: uv sync failed - dependencies may be out of date")
|
print("WARNING: uv sync failed - dependencies may be out of date")
|
||||||
print("Make sure uv is installed on Pi: curl -LsSf https://astral.sh/uv/install.sh | sh")
|
print("Make sure uv is installed on Pi: curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||||
|
|
||||||
|
# Deploy service file if it exists
|
||||||
|
if SERVICE_FILE.exists():
|
||||||
|
print()
|
||||||
|
print("Deploying systemd service file...")
|
||||||
|
run(["scp", str(SERVICE_FILE), f"{ssh_target}:/tmp/"])
|
||||||
|
run([
|
||||||
|
"ssh", ssh_target,
|
||||||
|
f"sudo mv /tmp/{SERVICE_FILE.name} /etc/systemd/system/ && sudo systemctl daemon-reload"
|
||||||
|
])
|
||||||
|
|
||||||
# Restart service if requested
|
# Restart service if requested
|
||||||
if restart:
|
if restart:
|
||||||
print()
|
print()
|
||||||
|
|||||||
Reference in New Issue
Block a user