Compare commits
9 Commits
59f8ebe61d
...
Cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0844adbcbe | ||
|
|
8233304ee2 | ||
|
|
2a6355033e | ||
|
|
10c200f994 | ||
|
|
031e1c3415 | ||
|
|
b5134098c0 | ||
|
|
c5e418eabc | ||
|
|
fe72619931 | ||
|
|
16bfc1e0e1 |
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Large binary / image files (do not commit)
|
||||
*.img.xz
|
||||
*.img.xz.bak
|
||||
*.img
|
||||
!emmc-provisioning/network-boot-initramfs/*.img
|
||||
|
||||
# Deploy logs (generated by deploy-to-proxmox.sh when DEPLOY_LOG=1)
|
||||
emmc-provisioning/scripts/deploy-*.log
|
||||
|
||||
# Backup/data from devices (large DBs and logs)
|
||||
backup-from-device/**/data/*.db
|
||||
backup-from-device/**/logs/
|
||||
**/*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
@@ -22,6 +22,7 @@ A single **revision number** is kept in `REVISION` and in a comment line in trac
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Read **emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md** for full setup.
|
||||
2. Use **emmc-provisioning/scripts/sync-portal-files-to-lxc.sh** to sync first-boot assets (including kiosk) to the file server.
|
||||
3. Provision devices via USB boot or network boot; first-boot configures kiosk, labwc, rotation, wallpaper, dark theme, and optional CM4 boot order.
|
||||
1. **New deployment:** Follow **[emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md](emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md)** for step-by-step instructions (Proxmox host prep → LXC deploy → network boot → portal files).
|
||||
2. **Sync first-boot assets** to the file server after deploy:
|
||||
`./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>`
|
||||
3. Provision devices via USB boot or network boot; first-boot configures the Chromium kiosk, labwc Wayland desktop, screen rotation, wallpaper, dark theme, and CM4 boot order.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Archive
|
||||
|
||||
This folder holds files that are no longer part of the active reTerminal DM4 / eMMC provisioning workflow. Kept for reference only.
|
||||
This folder holds files that are no longer part of the active reTerminal DM4 / eMMC provisioning workflow. Kept for historical reference only.
|
||||
|
||||
| Subfolder | Contents |
|
||||
|-----------|----------|
|
||||
| **chromium-setup-legacy/** | Old Chromium-setup guides and scripts: KDE installation, LED/buzzer control, audio config, touchscreen options, Flask apps, test scripts, revert-to-lxde. Kiosk assets (start-chromium.sh, chromium-kiosk.desktop) live in `emmc-provisioning/cloud-init/` and `emmc-provisioning/cloud-init/config-files/`. |
|
||||
| **cloud-init-duplicates/** | Duplicate or superseded cloud-init files (e.g. plymouth-custom.script duplicate of `files-from-guard/plymouth-custom/custom.script`). |
|
||||
| **chromium-setup-legacy/** | Old Chromium-setup guides and scripts: KDE installation, LED/buzzer control, audio config, touchscreen options, Flask apps, test scripts, revert-to-lxde. The active kiosk launcher lives at `emmc-provisioning/cloud-init/fileserver/start-chromium.sh` (Wayland/labwc). |
|
||||
|
||||
Do not rely on archived files for deployment; use the main tree under **emmc-provisioning/**.
|
||||
Do not rely on archived files for deployment; use the active tree under **emmc-provisioning/**.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
screen_width = Window.GetWidth();
|
||||
screen_height = Window.GetHeight();
|
||||
|
||||
theme_image = Image("splash.png");
|
||||
image_width = theme_image.GetWidth();
|
||||
image_height = theme_image.GetHeight();
|
||||
|
||||
scale_x = image_width / screen_width;
|
||||
scale_y = image_height / screen_height;
|
||||
|
||||
if (scale_x > 1 || scale_y > 1)
|
||||
{
|
||||
if (scale_x > scale_y)
|
||||
{
|
||||
resized_image = theme_image.Scale(screen_width, image_height / scale_x);
|
||||
image_x = 0;
|
||||
image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
resized_image = theme_image.Scale(image_width / scale_y, screen_height);
|
||||
image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2;
|
||||
image_y = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resized_image = theme_image.Scale(image_width, image_height);
|
||||
image_x = (screen_width - image_width) / 2;
|
||||
image_y = (screen_height - image_height) / 2;
|
||||
}
|
||||
|
||||
if (Plymouth.GetMode() != "shutdown")
|
||||
{
|
||||
sprite = Sprite(resized_image);
|
||||
sprite.SetPosition(image_x, image_y, -100);
|
||||
}
|
||||
|
||||
fun message_callback(text) {
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
||||
{"timestamp":"2026-02-23T22:46:10.124927","level":"INFO","source":"system","message":"GNSS Guard starting","data":{"config":{"asset_name":"OFFICE_LAB","iteration_period_seconds":30,"stale_threshold_seconds":60,"validation_threshold_meters":200.0,"startup_warmup_seconds":5,"positions_raw_retention_days":5,"positions_validation_retention_days":5,"log_retention_days":14,"tm_ais_url":"https://localhost:8443/location","tm_ais_enabled":true,"tm_ais_max_retries":1,"starlink_ip":"10.130.60.70","starlink_port":9200,"starlink_enabled":true,"starlink_max_retries":1,"nmea_primary_enabled":true,"nmea_secondary_enabled":true,"database_path":"data/gnss_guard.db","logs_base_path":"logs","web_enabled":true,"web_host":"0.0.0.0","web_port":8080,"web_show_route":true}}}
|
||||
{"timestamp":"2026-02-23T22:46:10.126502","level":"INFO","source":"system","message":"DEMO_UNIT mode - no database writes"}
|
||||
{"timestamp":"2026-02-23T22:46:10.127389","level":"INFO","source":"system","message":"Startup warm-up period","data":{"warmup_seconds":5}}
|
||||
{"timestamp":"2026-02-23T22:46:10.127998","level":"INFO","source":"nmea_primary","message":"Starting connection to device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001}}
|
||||
{"timestamp":"2026-02-23T22:46:10.129255","level":"ERROR","source":"nmea_primary","message":"Network error for device nmea_primary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4001}}
|
||||
{"timestamp":"2026-02-23T22:46:10.130714","level":"INFO","source":"nmea_secondary","message":"Starting connection to device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002}}
|
||||
{"timestamp":"2026-02-23T22:46:10.132229","level":"ERROR","source":"nmea_secondary","message":"Network error for device nmea_secondary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4002}}
|
||||
{"timestamp":"2026-02-23T22:46:15.136020","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:46:15.135561+00:00","timestamp_unix":1771879575.135561}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.136213","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:46:15.135588+00:00","timestamp_unix":1771879575.135588}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.136326","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:46:15.135575+00:00","timestamp_unix":1771879575.135575}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.136420","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:46:15.135475+00:00","timestamp_unix":1771879575.135475}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.136508","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:46:15.135543+00:00","timestamp_unix":1771879575.135543}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.155432","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:46:15.136736+00:00","validation_timestamp_unix":1771879575.136736,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:46:15.135561+00:00","timestamp_unix":1771879575.135561},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:46:15.135588+00:00","timestamp_unix":1771879575.135588},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:46:15.135575+00:00","timestamp_unix":1771879575.135575},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:46:15.135475+00:00","timestamp_unix":1771879575.135475},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:46:15.135543+00:00","timestamp_unix":1771879575.135543}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:46:15.155998","level":"WARNING","source":"buzzer","message":"Alarm started: GPS jamming/spoofing detected (distance exceeded threshold)"}
|
||||
{"timestamp":"2026-02-23T22:46:15.159100","level":"ERROR","source":"nmea_primary","message":"Network error for device nmea_primary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4001}}
|
||||
{"timestamp":"2026-02-23T22:46:15.161128","level":"ERROR","source":"nmea_secondary","message":"Network error for device nmea_secondary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4002}}
|
||||
{"timestamp":"2026-02-23T22:46:30.162167","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:46:30.163835","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:46:45.208056","level":"INFO","source":"system","message":"GNSS Guard stopped"}
|
||||
{"timestamp":"2026-02-23T22:47:18.807086","level":"INFO","source":"system","message":"GNSS Guard starting","data":{"config":{"asset_name":"OFFICE_LAB","iteration_period_seconds":30,"stale_threshold_seconds":60,"validation_threshold_meters":200.0,"startup_warmup_seconds":5,"positions_raw_retention_days":5,"positions_validation_retention_days":5,"log_retention_days":14,"tm_ais_url":"https://localhost:8443/location","tm_ais_enabled":true,"tm_ais_max_retries":1,"starlink_ip":"10.130.60.70","starlink_port":9200,"starlink_enabled":true,"starlink_max_retries":1,"nmea_primary_enabled":true,"nmea_secondary_enabled":true,"database_path":"data/gnss_guard.db","logs_base_path":"logs","web_enabled":true,"web_host":"0.0.0.0","web_port":8080,"web_show_route":true}}}
|
||||
{"timestamp":"2026-02-23T22:47:18.809486","level":"INFO","source":"system","message":"DEMO_UNIT mode - no database writes"}
|
||||
{"timestamp":"2026-02-23T22:47:18.810620","level":"INFO","source":"system","message":"Startup warm-up period","data":{"warmup_seconds":5}}
|
||||
{"timestamp":"2026-02-23T22:47:18.811272","level":"INFO","source":"nmea_primary","message":"Starting connection to device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001}}
|
||||
{"timestamp":"2026-02-23T22:47:18.812711","level":"ERROR","source":"nmea_primary","message":"Network error for device nmea_primary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4001}}
|
||||
{"timestamp":"2026-02-23T22:47:18.813713","level":"INFO","source":"nmea_secondary","message":"Starting connection to device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002}}
|
||||
{"timestamp":"2026-02-23T22:47:18.814929","level":"ERROR","source":"nmea_secondary","message":"Network error for device nmea_secondary","data":{"error":"[Errno 101] Network is unreachable","device_ip":"10.130.60.61","device_port":4002}}
|
||||
{"timestamp":"2026-02-23T22:47:23.824661","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:47:23.823939+00:00","timestamp_unix":1771879643.823939}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.825526","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:47:23.823968+00:00","timestamp_unix":1771879643.823968}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.826240","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:47:23.823954+00:00","timestamp_unix":1771879643.823954}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.826556","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:47:23.823813+00:00","timestamp_unix":1771879643.823813}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.826728","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:47:23.823918+00:00","timestamp_unix":1771879643.823918}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.875512","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:47:23.827103+00:00","validation_timestamp_unix":1771879643.827103,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:23.823939+00:00","timestamp_unix":1771879643.823939},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:47:23.823968+00:00","timestamp_unix":1771879643.823968},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:23.823954+00:00","timestamp_unix":1771879643.823954},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:23.823813+00:00","timestamp_unix":1771879643.823813},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:23.823918+00:00","timestamp_unix":1771879643.823918}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:47:23.878079","level":"WARNING","source":"buzzer","message":"Alarm started: GPS jamming/spoofing detected (distance exceeded threshold)"}
|
||||
{"timestamp":"2026-02-23T22:47:33.882690","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:47:33.887174","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:47:48.885387","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:47:48.889741","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:47:53.815054","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:47:53.813529+00:00","timestamp_unix":1771879673.813529}}}
|
||||
{"timestamp":"2026-02-23T22:47:53.815319","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:47:53.813556+00:00","timestamp_unix":1771879673.813556}}}
|
||||
{"timestamp":"2026-02-23T22:47:53.815443","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:47:53.813543+00:00","timestamp_unix":1771879673.813543}}}
|
||||
{"timestamp":"2026-02-23T22:47:53.815600","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:47:53.813436+00:00","timestamp_unix":1771879673.813436}}}
|
||||
{"timestamp":"2026-02-23T22:47:53.815698","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:47:53.813514+00:00","timestamp_unix":1771879673.813514}}}
|
||||
{"timestamp":"2026-02-23T22:47:53.864526","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:47:53.816048+00:00","validation_timestamp_unix":1771879673.816048,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:53.813529+00:00","timestamp_unix":1771879673.813529},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:47:53.813556+00:00","timestamp_unix":1771879673.813556},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:53.813543+00:00","timestamp_unix":1771879673.813543},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:53.813436+00:00","timestamp_unix":1771879673.813436},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:47:53.813514+00:00","timestamp_unix":1771879673.813514}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:48:03.888362","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:03.892844","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:18.890968","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:18.895195","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:23.815763","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:48:23.815386+00:00","timestamp_unix":1771879703.815386}}}
|
||||
{"timestamp":"2026-02-23T22:48:23.815927","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:48:23.815413+00:00","timestamp_unix":1771879703.815413}}}
|
||||
{"timestamp":"2026-02-23T22:48:23.816016","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:48:23.815400+00:00","timestamp_unix":1771879703.8154}}}
|
||||
{"timestamp":"2026-02-23T22:48:23.816099","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:48:23.815321+00:00","timestamp_unix":1771879703.815321}}}
|
||||
{"timestamp":"2026-02-23T22:48:23.816177","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:48:23.815372+00:00","timestamp_unix":1771879703.815372}}}
|
||||
{"timestamp":"2026-02-23T22:48:23.848734","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:48:23.816335+00:00","validation_timestamp_unix":1771879703.816335,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:23.815386+00:00","timestamp_unix":1771879703.815386},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:48:23.815413+00:00","timestamp_unix":1771879703.815413},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:23.815400+00:00","timestamp_unix":1771879703.8154},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:23.815321+00:00","timestamp_unix":1771879703.815321},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:23.815372+00:00","timestamp_unix":1771879703.815372}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:48:33.893803","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:33.898407","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:48.897743","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:48.901615","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:48:53.818367","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:48:53.817149+00:00","timestamp_unix":1771879733.817149}}}
|
||||
{"timestamp":"2026-02-23T22:48:53.819643","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:48:53.817250+00:00","timestamp_unix":1771879733.81725}}}
|
||||
{"timestamp":"2026-02-23T22:48:53.819892","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:48:53.817164+00:00","timestamp_unix":1771879733.817164}}}
|
||||
{"timestamp":"2026-02-23T22:48:53.820055","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:48:53.817059+00:00","timestamp_unix":1771879733.817059}}}
|
||||
{"timestamp":"2026-02-23T22:48:53.820190","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:48:53.817132+00:00","timestamp_unix":1771879733.817132}}}
|
||||
{"timestamp":"2026-02-23T22:48:53.844895","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:48:53.820550+00:00","validation_timestamp_unix":1771879733.82055,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:53.817149+00:00","timestamp_unix":1771879733.817149},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:48:53.817250+00:00","timestamp_unix":1771879733.81725},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:53.817164+00:00","timestamp_unix":1771879733.817164},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:53.817059+00:00","timestamp_unix":1771879733.817059},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:48:53.817132+00:00","timestamp_unix":1771879733.817132}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:49:03.901559","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:03.905116","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:18.904130","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:18.908554","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:23.817871","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:49:23.817537+00:00","timestamp_unix":1771879763.817537}}}
|
||||
{"timestamp":"2026-02-23T22:49:23.818151","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:49:23.817586+00:00","timestamp_unix":1771879763.817586}}}
|
||||
{"timestamp":"2026-02-23T22:49:23.818320","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:49:23.817550+00:00","timestamp_unix":1771879763.81755}}}
|
||||
{"timestamp":"2026-02-23T22:49:23.818428","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:49:23.817476+00:00","timestamp_unix":1771879763.817476}}}
|
||||
{"timestamp":"2026-02-23T22:49:23.818517","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:49:23.817522+00:00","timestamp_unix":1771879763.817522}}}
|
||||
{"timestamp":"2026-02-23T22:49:23.846511","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:49:23.818835+00:00","validation_timestamp_unix":1771879763.818835,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:23.817537+00:00","timestamp_unix":1771879763.817537},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:49:23.817586+00:00","timestamp_unix":1771879763.817586},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:23.817550+00:00","timestamp_unix":1771879763.81755},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:23.817476+00:00","timestamp_unix":1771879763.817476},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:23.817522+00:00","timestamp_unix":1771879763.817522}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:49:33.906784","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:33.911253","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:48.910414","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:48.915018","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:49:53.818928","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:49:53.818612+00:00","timestamp_unix":1771879793.818612}}}
|
||||
{"timestamp":"2026-02-23T22:49:53.819100","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:49:53.818637+00:00","timestamp_unix":1771879793.818637}}}
|
||||
{"timestamp":"2026-02-23T22:49:53.819194","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:49:53.818625+00:00","timestamp_unix":1771879793.818625}}}
|
||||
{"timestamp":"2026-02-23T22:49:53.819281","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:49:53.818549+00:00","timestamp_unix":1771879793.818549}}}
|
||||
{"timestamp":"2026-02-23T22:49:53.819363","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:49:53.818596+00:00","timestamp_unix":1771879793.818596}}}
|
||||
{"timestamp":"2026-02-23T22:49:53.853132","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:49:53.819539+00:00","validation_timestamp_unix":1771879793.819539,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:53.818612+00:00","timestamp_unix":1771879793.818612},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:49:53.818637+00:00","timestamp_unix":1771879793.818637},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:53.818625+00:00","timestamp_unix":1771879793.818625},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:53.818549+00:00","timestamp_unix":1771879793.818549},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:49:53.818596+00:00","timestamp_unix":1771879793.818596}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:50:03.912836","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:03.917970","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:18.916354","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:18.920628","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:23.820204","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:50:23.819162+00:00","timestamp_unix":1771879823.819162}}}
|
||||
{"timestamp":"2026-02-23T22:50:23.820443","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:50:23.819189+00:00","timestamp_unix":1771879823.819189}}}
|
||||
{"timestamp":"2026-02-23T22:50:23.820584","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:50:23.819176+00:00","timestamp_unix":1771879823.819176}}}
|
||||
{"timestamp":"2026-02-23T22:50:23.820687","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:50:23.819082+00:00","timestamp_unix":1771879823.819082}}}
|
||||
{"timestamp":"2026-02-23T22:50:23.821320","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:50:23.819146+00:00","timestamp_unix":1771879823.819146}}}
|
||||
{"timestamp":"2026-02-23T22:50:23.854158","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:50:23.822277+00:00","validation_timestamp_unix":1771879823.822277,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:23.819162+00:00","timestamp_unix":1771879823.819162},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:50:23.819189+00:00","timestamp_unix":1771879823.819189},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:23.819176+00:00","timestamp_unix":1771879823.819176},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:23.819082+00:00","timestamp_unix":1771879823.819082},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:23.819146+00:00","timestamp_unix":1771879823.819146}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:50:33.920836","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:33.924320","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:48.923070","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:48.927416","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:50:53.820337","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:50:53.819936+00:00","timestamp_unix":1771879853.819936}}}
|
||||
{"timestamp":"2026-02-23T22:50:53.820578","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:50:53.819963+00:00","timestamp_unix":1771879853.819963}}}
|
||||
{"timestamp":"2026-02-23T22:50:53.820695","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:50:53.819950+00:00","timestamp_unix":1771879853.81995}}}
|
||||
{"timestamp":"2026-02-23T22:50:53.820789","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:50:53.819869+00:00","timestamp_unix":1771879853.819869}}}
|
||||
{"timestamp":"2026-02-23T22:50:53.820876","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:50:53.819921+00:00","timestamp_unix":1771879853.819921}}}
|
||||
{"timestamp":"2026-02-23T22:50:53.844653","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:50:53.821110+00:00","validation_timestamp_unix":1771879853.82111,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:53.819936+00:00","timestamp_unix":1771879853.819936},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:50:53.819963+00:00","timestamp_unix":1771879853.819963},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:53.819950+00:00","timestamp_unix":1771879853.81995},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:53.819869+00:00","timestamp_unix":1771879853.819869},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:50:53.819921+00:00","timestamp_unix":1771879853.819921}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:51:03.926307","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:03.931205","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:18.929675","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:18.933639","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:23.821611","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:51:23.821100+00:00","timestamp_unix":1771879883.8211}}}
|
||||
{"timestamp":"2026-02-23T22:51:23.821810","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:51:23.821127+00:00","timestamp_unix":1771879883.821127}}}
|
||||
{"timestamp":"2026-02-23T22:51:23.821995","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:51:23.821114+00:00","timestamp_unix":1771879883.821114}}}
|
||||
{"timestamp":"2026-02-23T22:51:23.822107","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:51:23.821008+00:00","timestamp_unix":1771879883.821008}}}
|
||||
{"timestamp":"2026-02-23T22:51:23.822201","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:51:23.821084+00:00","timestamp_unix":1771879883.821084}}}
|
||||
{"timestamp":"2026-02-23T22:51:23.847655","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:51:23.822494+00:00","validation_timestamp_unix":1771879883.822494,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:23.821100+00:00","timestamp_unix":1771879883.8211},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:51:23.821127+00:00","timestamp_unix":1771879883.821127},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:23.821114+00:00","timestamp_unix":1771879883.821114},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:23.821008+00:00","timestamp_unix":1771879883.821008},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:23.821084+00:00","timestamp_unix":1771879883.821084}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:51:33.933258","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:33.936137","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:48.936125","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:48.940466","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:51:53.822790","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:51:53.822258+00:00","timestamp_unix":1771879913.822258}}}
|
||||
{"timestamp":"2026-02-23T22:51:53.822991","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:51:53.822285+00:00","timestamp_unix":1771879913.822285}}}
|
||||
{"timestamp":"2026-02-23T22:51:53.823106","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:51:53.822272+00:00","timestamp_unix":1771879913.822272}}}
|
||||
{"timestamp":"2026-02-23T22:51:53.823205","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:51:53.822177+00:00","timestamp_unix":1771879913.822177}}}
|
||||
{"timestamp":"2026-02-23T22:51:53.823339","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:51:53.822242+00:00","timestamp_unix":1771879913.822242}}}
|
||||
{"timestamp":"2026-02-23T22:51:53.850451","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:51:53.823625+00:00","validation_timestamp_unix":1771879913.823625,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:53.822258+00:00","timestamp_unix":1771879913.822258},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:51:53.822285+00:00","timestamp_unix":1771879913.822285},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:53.822272+00:00","timestamp_unix":1771879913.822272},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:53.822177+00:00","timestamp_unix":1771879913.822177},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:51:53.822242+00:00","timestamp_unix":1771879913.822242}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:52:03.940344","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:03.943838","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:18.943750","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:18.948782","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:23.825508","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:52:23.824981+00:00","timestamp_unix":1771879943.824981}}}
|
||||
{"timestamp":"2026-02-23T22:52:23.825749","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:52:23.825010+00:00","timestamp_unix":1771879943.82501}}}
|
||||
{"timestamp":"2026-02-23T22:52:23.825856","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:52:23.824996+00:00","timestamp_unix":1771879943.824996}}}
|
||||
{"timestamp":"2026-02-23T22:52:23.826033","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:52:23.824888+00:00","timestamp_unix":1771879943.824888}}}
|
||||
{"timestamp":"2026-02-23T22:52:23.826148","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:52:23.824964+00:00","timestamp_unix":1771879943.824964}}}
|
||||
{"timestamp":"2026-02-23T22:52:23.860023","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:52:23.826462+00:00","validation_timestamp_unix":1771879943.826462,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:23.824981+00:00","timestamp_unix":1771879943.824981},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:52:23.825010+00:00","timestamp_unix":1771879943.82501},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:23.824996+00:00","timestamp_unix":1771879943.824996},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:23.824888+00:00","timestamp_unix":1771879943.824888},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:23.824964+00:00","timestamp_unix":1771879943.824964}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:52:33.946170","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:33.951853","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:48.950165","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:48.954796","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:52:53.826384","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:52:53.825839+00:00","timestamp_unix":1771879973.825839}}}
|
||||
{"timestamp":"2026-02-23T22:52:53.826549","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:52:53.825867+00:00","timestamp_unix":1771879973.825867}}}
|
||||
{"timestamp":"2026-02-23T22:52:53.826648","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:52:53.825853+00:00","timestamp_unix":1771879973.825853}}}
|
||||
{"timestamp":"2026-02-23T22:52:53.826738","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:52:53.825765+00:00","timestamp_unix":1771879973.825765}}}
|
||||
{"timestamp":"2026-02-23T22:52:53.826821","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:52:53.825821+00:00","timestamp_unix":1771879973.825821}}}
|
||||
{"timestamp":"2026-02-23T22:52:53.856580","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:52:53.826994+00:00","validation_timestamp_unix":1771879973.826994,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:53.825839+00:00","timestamp_unix":1771879973.825839},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:52:53.825867+00:00","timestamp_unix":1771879973.825867},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:53.825853+00:00","timestamp_unix":1771879973.825853},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:53.825765+00:00","timestamp_unix":1771879973.825765},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:52:53.825821+00:00","timestamp_unix":1771879973.825821}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:53:03.953648","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:03.957135","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:18.956256","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:18.963561","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:23.826822","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:53:23.826229+00:00","timestamp_unix":1771880003.826229}}}
|
||||
{"timestamp":"2026-02-23T22:53:23.827172","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:53:23.826257+00:00","timestamp_unix":1771880003.826257}}}
|
||||
{"timestamp":"2026-02-23T22:53:23.827319","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:53:23.826243+00:00","timestamp_unix":1771880003.826243}}}
|
||||
{"timestamp":"2026-02-23T22:53:23.827460","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:53:23.826151+00:00","timestamp_unix":1771880003.826151}}}
|
||||
{"timestamp":"2026-02-23T22:53:23.827570","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:53:23.826212+00:00","timestamp_unix":1771880003.826212}}}
|
||||
{"timestamp":"2026-02-23T22:53:23.864544","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:53:23.827891+00:00","validation_timestamp_unix":1771880003.827891,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:23.826229+00:00","timestamp_unix":1771880003.826229},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:53:23.826257+00:00","timestamp_unix":1771880003.826257},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:23.826243+00:00","timestamp_unix":1771880003.826243},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:23.826151+00:00","timestamp_unix":1771880003.826151},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:23.826212+00:00","timestamp_unix":1771880003.826212}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:53:33.958745","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:33.966759","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:48.962622","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:48.969076","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:53:53.827130","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:53:53.826776+00:00","timestamp_unix":1771880033.826776}}}
|
||||
{"timestamp":"2026-02-23T22:53:53.827303","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:53:53.826802+00:00","timestamp_unix":1771880033.826802}}}
|
||||
{"timestamp":"2026-02-23T22:53:53.827397","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:53:53.826790+00:00","timestamp_unix":1771880033.82679}}}
|
||||
{"timestamp":"2026-02-23T22:53:53.827484","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:53:53.826705+00:00","timestamp_unix":1771880033.826705}}}
|
||||
{"timestamp":"2026-02-23T22:53:53.827567","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:53:53.826762+00:00","timestamp_unix":1771880033.826762}}}
|
||||
{"timestamp":"2026-02-23T22:53:53.856027","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:53:53.827740+00:00","validation_timestamp_unix":1771880033.82774,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:53.826776+00:00","timestamp_unix":1771880033.826776},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:53:53.826802+00:00","timestamp_unix":1771880033.826802},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:53.826790+00:00","timestamp_unix":1771880033.82679},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:53.826705+00:00","timestamp_unix":1771880033.826705},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:53:53.826762+00:00","timestamp_unix":1771880033.826762}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:54:03.965987","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:03.971379","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:18.969350","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:18.973833","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:23.828331","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:54:23.827104+00:00","timestamp_unix":1771880063.827104}}}
|
||||
{"timestamp":"2026-02-23T22:54:23.828631","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:54:23.827133+00:00","timestamp_unix":1771880063.827133}}}
|
||||
{"timestamp":"2026-02-23T22:54:23.828814","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:54:23.827119+00:00","timestamp_unix":1771880063.827119}}}
|
||||
{"timestamp":"2026-02-23T22:54:23.828941","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:54:23.827012+00:00","timestamp_unix":1771880063.827012}}}
|
||||
{"timestamp":"2026-02-23T22:54:23.829057","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:54:23.827065+00:00","timestamp_unix":1771880063.827065}}}
|
||||
{"timestamp":"2026-02-23T22:54:23.856635","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:54:23.829409+00:00","validation_timestamp_unix":1771880063.829409,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:23.827104+00:00","timestamp_unix":1771880063.827104},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:54:23.827133+00:00","timestamp_unix":1771880063.827133},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:23.827119+00:00","timestamp_unix":1771880063.827119},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:23.827012+00:00","timestamp_unix":1771880063.827012},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:23.827065+00:00","timestamp_unix":1771880063.827065}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:54:33.972774","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:33.977129","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:48.976540","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:48.979882","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:54:53.829267","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:54:53.828852+00:00","timestamp_unix":1771880093.828852}}}
|
||||
{"timestamp":"2026-02-23T22:54:53.829465","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:54:53.828879+00:00","timestamp_unix":1771880093.828879}}}
|
||||
{"timestamp":"2026-02-23T22:54:53.829608","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:54:53.828866+00:00","timestamp_unix":1771880093.828866}}}
|
||||
{"timestamp":"2026-02-23T22:54:53.830195","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:54:53.828783+00:00","timestamp_unix":1771880093.828783}}}
|
||||
{"timestamp":"2026-02-23T22:54:53.830602","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:54:53.828836+00:00","timestamp_unix":1771880093.828836}}}
|
||||
{"timestamp":"2026-02-23T22:54:53.855158","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:54:53.831265+00:00","validation_timestamp_unix":1771880093.831265,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:53.828852+00:00","timestamp_unix":1771880093.828852},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:54:53.828879+00:00","timestamp_unix":1771880093.828879},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:53.828866+00:00","timestamp_unix":1771880093.828866},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:53.828783+00:00","timestamp_unix":1771880093.828783},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:54:53.828836+00:00","timestamp_unix":1771880093.828836}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:55:03.979193","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:03.981318","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:18.982800","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:18.983832","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:23.830806","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:55:23.830222+00:00","timestamp_unix":1771880123.830222}}}
|
||||
{"timestamp":"2026-02-23T22:55:23.831062","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:55:23.830252+00:00","timestamp_unix":1771880123.830252}}}
|
||||
{"timestamp":"2026-02-23T22:55:23.831190","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:55:23.830237+00:00","timestamp_unix":1771880123.830237}}}
|
||||
{"timestamp":"2026-02-23T22:55:23.831300","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:55:23.830133+00:00","timestamp_unix":1771880123.830133}}}
|
||||
{"timestamp":"2026-02-23T22:55:23.831433","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:55:23.830206+00:00","timestamp_unix":1771880123.830206}}}
|
||||
{"timestamp":"2026-02-23T22:55:23.861645","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:55:23.831756+00:00","validation_timestamp_unix":1771880123.831756,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:23.830222+00:00","timestamp_unix":1771880123.830222},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:55:23.830252+00:00","timestamp_unix":1771880123.830252},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:23.830237+00:00","timestamp_unix":1771880123.830237},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:23.830133+00:00","timestamp_unix":1771880123.830133},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:23.830206+00:00","timestamp_unix":1771880123.830206}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:55:33.987349","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:33.988409","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:48.991360","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:48.992540","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:55:53.831586","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:55:53.831061+00:00","timestamp_unix":1771880153.831061}}}
|
||||
{"timestamp":"2026-02-23T22:55:53.831837","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:55:53.831092+00:00","timestamp_unix":1771880153.831092}}}
|
||||
{"timestamp":"2026-02-23T22:55:53.831958","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:55:53.831076+00:00","timestamp_unix":1771880153.831076}}}
|
||||
{"timestamp":"2026-02-23T22:55:53.832062","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:55:53.830992+00:00","timestamp_unix":1771880153.830992}}}
|
||||
{"timestamp":"2026-02-23T22:55:53.832152","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:55:53.831045+00:00","timestamp_unix":1771880153.831045}}}
|
||||
{"timestamp":"2026-02-23T22:55:53.867050","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:55:53.832504+00:00","validation_timestamp_unix":1771880153.832504,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:53.831061+00:00","timestamp_unix":1771880153.831061},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:55:53.831092+00:00","timestamp_unix":1771880153.831092},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:53.831076+00:00","timestamp_unix":1771880153.831076},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:53.830992+00:00","timestamp_unix":1771880153.830992},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:55:53.831045+00:00","timestamp_unix":1771880153.831045}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:56:03.995636","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:03.996791","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:18.999583","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:19.001025","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:23.832144","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:56:23.831294+00:00","timestamp_unix":1771880183.831294}}}
|
||||
{"timestamp":"2026-02-23T22:56:23.832506","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:56:23.831335+00:00","timestamp_unix":1771880183.831335}}}
|
||||
{"timestamp":"2026-02-23T22:56:23.832765","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:56:23.831315+00:00","timestamp_unix":1771880183.831315}}}
|
||||
{"timestamp":"2026-02-23T22:56:23.834116","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:56:23.831204+00:00","timestamp_unix":1771880183.831204}}}
|
||||
{"timestamp":"2026-02-23T22:56:23.834952","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:56:23.831271+00:00","timestamp_unix":1771880183.831271}}}
|
||||
{"timestamp":"2026-02-23T22:56:23.867265","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:56:23.836169+00:00","validation_timestamp_unix":1771880183.836169,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:23.831294+00:00","timestamp_unix":1771880183.831294},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:56:23.831335+00:00","timestamp_unix":1771880183.831335},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:23.831315+00:00","timestamp_unix":1771880183.831315},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:23.831204+00:00","timestamp_unix":1771880183.831204},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:23.831271+00:00","timestamp_unix":1771880183.831271}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:56:34.002920","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:34.004207","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:49.005381","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:49.006432","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:56:53.832575","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:56:53.831992+00:00","timestamp_unix":1771880213.831992}}}
|
||||
{"timestamp":"2026-02-23T22:56:53.832800","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:56:53.832021+00:00","timestamp_unix":1771880213.832021}}}
|
||||
{"timestamp":"2026-02-23T22:56:53.832936","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:56:53.832007+00:00","timestamp_unix":1771880213.832007}}}
|
||||
{"timestamp":"2026-02-23T22:56:53.833051","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:56:53.831905+00:00","timestamp_unix":1771880213.831905}}}
|
||||
{"timestamp":"2026-02-23T22:56:53.833144","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:56:53.831975+00:00","timestamp_unix":1771880213.831975}}}
|
||||
{"timestamp":"2026-02-23T22:56:53.859002","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:56:53.833453+00:00","validation_timestamp_unix":1771880213.833453,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:53.831992+00:00","timestamp_unix":1771880213.831992},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:56:53.832021+00:00","timestamp_unix":1771880213.832021},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:53.832007+00:00","timestamp_unix":1771880213.832007},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:53.831905+00:00","timestamp_unix":1771880213.831905},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:56:53.831975+00:00","timestamp_unix":1771880213.831975}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:57:04.008483","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:04.009633","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:19.011357","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:19.011972","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:23.835296","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:57:23.834427+00:00","timestamp_unix":1771880243.834427}}}
|
||||
{"timestamp":"2026-02-23T22:57:23.835685","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:57:23.834459+00:00","timestamp_unix":1771880243.834459}}}
|
||||
{"timestamp":"2026-02-23T22:57:23.835962","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:57:23.834444+00:00","timestamp_unix":1771880243.834444}}}
|
||||
{"timestamp":"2026-02-23T22:57:23.836175","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:57:23.833555+00:00","timestamp_unix":1771880243.833555}}}
|
||||
{"timestamp":"2026-02-23T22:57:23.837601","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:57:23.834383+00:00","timestamp_unix":1771880243.834383}}}
|
||||
{"timestamp":"2026-02-23T22:57:23.869028","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:57:23.838126+00:00","validation_timestamp_unix":1771880243.838126,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:23.834427+00:00","timestamp_unix":1771880243.834427},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:57:23.834459+00:00","timestamp_unix":1771880243.834459},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:23.834444+00:00","timestamp_unix":1771880243.834444},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:23.833555+00:00","timestamp_unix":1771880243.833555},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:23.834383+00:00","timestamp_unix":1771880243.834383}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:57:34.015167","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:34.016507","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:49.018784","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:49.019705","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:57:53.834172","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:57:53.833609+00:00","timestamp_unix":1771880273.833609}}}
|
||||
{"timestamp":"2026-02-23T22:57:53.834379","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:57:53.833635+00:00","timestamp_unix":1771880273.833635}}}
|
||||
{"timestamp":"2026-02-23T22:57:53.834480","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:57:53.833622+00:00","timestamp_unix":1771880273.833622}}}
|
||||
{"timestamp":"2026-02-23T22:57:53.834573","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:57:53.833540+00:00","timestamp_unix":1771880273.83354}}}
|
||||
{"timestamp":"2026-02-23T22:57:53.834657","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:57:53.833593+00:00","timestamp_unix":1771880273.833593}}}
|
||||
{"timestamp":"2026-02-23T22:57:53.867179","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:57:53.834907+00:00","validation_timestamp_unix":1771880273.834907,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:53.833609+00:00","timestamp_unix":1771880273.833609},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:57:53.833635+00:00","timestamp_unix":1771880273.833635},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:53.833622+00:00","timestamp_unix":1771880273.833622},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:53.833540+00:00","timestamp_unix":1771880273.83354},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:57:53.833593+00:00","timestamp_unix":1771880273.833593}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:58:04.021584","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:04.022381","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:19.023519","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:19.024348","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:23.836982","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:58:23.836337+00:00","timestamp_unix":1771880303.836337}}}
|
||||
{"timestamp":"2026-02-23T22:58:23.837235","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:58:23.836365+00:00","timestamp_unix":1771880303.836365}}}
|
||||
{"timestamp":"2026-02-23T22:58:23.837375","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:58:23.836351+00:00","timestamp_unix":1771880303.836351}}}
|
||||
{"timestamp":"2026-02-23T22:58:23.837490","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:58:23.836250+00:00","timestamp_unix":1771880303.83625}}}
|
||||
{"timestamp":"2026-02-23T22:58:23.837591","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:58:23.836321+00:00","timestamp_unix":1771880303.836321}}}
|
||||
{"timestamp":"2026-02-23T22:58:23.862153","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:58:23.838005+00:00","validation_timestamp_unix":1771880303.838005,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:23.836337+00:00","timestamp_unix":1771880303.836337},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:58:23.836365+00:00","timestamp_unix":1771880303.836365},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:23.836351+00:00","timestamp_unix":1771880303.836351},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:23.836250+00:00","timestamp_unix":1771880303.83625},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:23.836321+00:00","timestamp_unix":1771880303.836321}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:58:34.026961","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:34.028162","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:49.030075","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:49.030863","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:58:53.836975","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:58:53.836644+00:00","timestamp_unix":1771880333.836644}}}
|
||||
{"timestamp":"2026-02-23T22:58:53.837138","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:58:53.836671+00:00","timestamp_unix":1771880333.836671}}}
|
||||
{"timestamp":"2026-02-23T22:58:53.837264","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:58:53.836658+00:00","timestamp_unix":1771880333.836658}}}
|
||||
{"timestamp":"2026-02-23T22:58:53.837348","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:58:53.836585+00:00","timestamp_unix":1771880333.836585}}}
|
||||
{"timestamp":"2026-02-23T22:58:53.837425","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:58:53.836629+00:00","timestamp_unix":1771880333.836629}}}
|
||||
{"timestamp":"2026-02-23T22:58:53.864992","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:58:53.837598+00:00","validation_timestamp_unix":1771880333.837598,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:53.836644+00:00","timestamp_unix":1771880333.836644},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:58:53.836671+00:00","timestamp_unix":1771880333.836671},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:53.836658+00:00","timestamp_unix":1771880333.836658},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:53.836585+00:00","timestamp_unix":1771880333.836585},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:58:53.836629+00:00","timestamp_unix":1771880333.836629}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:59:04.032734","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:04.033610","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:19.035139","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:19.036080","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:23.838471","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:59:23.837657+00:00","timestamp_unix":1771880363.837657}}}
|
||||
{"timestamp":"2026-02-23T22:59:23.838766","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:59:23.837684+00:00","timestamp_unix":1771880363.837684}}}
|
||||
{"timestamp":"2026-02-23T22:59:23.838943","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:59:23.837671+00:00","timestamp_unix":1771880363.837671}}}
|
||||
{"timestamp":"2026-02-23T22:59:23.839045","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:59:23.837597+00:00","timestamp_unix":1771880363.837597}}}
|
||||
{"timestamp":"2026-02-23T22:59:23.839131","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:59:23.837642+00:00","timestamp_unix":1771880363.837642}}}
|
||||
{"timestamp":"2026-02-23T22:59:23.868261","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:59:23.839429+00:00","validation_timestamp_unix":1771880363.839429,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:23.837657+00:00","timestamp_unix":1771880363.837657},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:59:23.837684+00:00","timestamp_unix":1771880363.837684},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:23.837671+00:00","timestamp_unix":1771880363.837671},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:23.837597+00:00","timestamp_unix":1771880363.837597},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:23.837642+00:00","timestamp_unix":1771880363.837642}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T22:59:34.039609","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:34.041450","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:49.042409","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:49.043482","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T22:59:53.840376","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T20:59:53.839026+00:00","timestamp_unix":1771880393.839026}}}
|
||||
{"timestamp":"2026-02-23T22:59:53.840666","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T20:59:53.839057+00:00","timestamp_unix":1771880393.839057}}}
|
||||
{"timestamp":"2026-02-23T22:59:53.840818","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T20:59:53.839043+00:00","timestamp_unix":1771880393.839043}}}
|
||||
{"timestamp":"2026-02-23T22:59:53.840931","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T20:59:53.838921+00:00","timestamp_unix":1771880393.838921}}}
|
||||
{"timestamp":"2026-02-23T22:59:53.841041","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T20:59:53.839008+00:00","timestamp_unix":1771880393.839008}}}
|
||||
{"timestamp":"2026-02-23T22:59:53.865632","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T20:59:53.841310+00:00","validation_timestamp_unix":1771880393.84131,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:53.839026+00:00","timestamp_unix":1771880393.839026},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T20:59:53.839057+00:00","timestamp_unix":1771880393.839057},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:53.839043+00:00","timestamp_unix":1771880393.839043},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:53.838921+00:00","timestamp_unix":1771880393.838921},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T20:59:53.839008+00:00","timestamp_unix":1771880393.839008}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:00:04.044800","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:04.045736","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:19.048243","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:19.049200","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:23.839341","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:00:23.839029+00:00","timestamp_unix":1771880423.839029}}}
|
||||
{"timestamp":"2026-02-23T23:00:23.839539","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:00:23.839057+00:00","timestamp_unix":1771880423.839057}}}
|
||||
{"timestamp":"2026-02-23T23:00:23.839631","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:00:23.839043+00:00","timestamp_unix":1771880423.839043}}}
|
||||
{"timestamp":"2026-02-23T23:00:23.839714","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:00:23.838969+00:00","timestamp_unix":1771880423.838969}}}
|
||||
{"timestamp":"2026-02-23T23:00:23.839792","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:00:23.839014+00:00","timestamp_unix":1771880423.839014}}}
|
||||
{"timestamp":"2026-02-23T23:00:23.868096","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:00:23.839955+00:00","validation_timestamp_unix":1771880423.839955,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:23.839029+00:00","timestamp_unix":1771880423.839029},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:00:23.839057+00:00","timestamp_unix":1771880423.839057},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:23.839043+00:00","timestamp_unix":1771880423.839043},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:23.838969+00:00","timestamp_unix":1771880423.838969},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:23.839014+00:00","timestamp_unix":1771880423.839014}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:00:34.051595","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:34.052676","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:49.054886","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:49.055525","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:00:53.841108","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:00:53.840773+00:00","timestamp_unix":1771880453.840773}}}
|
||||
{"timestamp":"2026-02-23T23:00:53.841252","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:00:53.840801+00:00","timestamp_unix":1771880453.840801}}}
|
||||
{"timestamp":"2026-02-23T23:00:53.841347","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:00:53.840787+00:00","timestamp_unix":1771880453.840787}}}
|
||||
{"timestamp":"2026-02-23T23:00:53.841435","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:00:53.840707+00:00","timestamp_unix":1771880453.840707}}}
|
||||
{"timestamp":"2026-02-23T23:00:53.841517","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:00:53.840757+00:00","timestamp_unix":1771880453.840757}}}
|
||||
{"timestamp":"2026-02-23T23:00:53.867713","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:00:53.841688+00:00","validation_timestamp_unix":1771880453.841688,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:53.840773+00:00","timestamp_unix":1771880453.840773},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:00:53.840801+00:00","timestamp_unix":1771880453.840801},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:53.840787+00:00","timestamp_unix":1771880453.840787},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:53.840707+00:00","timestamp_unix":1771880453.840707},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:00:53.840757+00:00","timestamp_unix":1771880453.840757}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:01:04.057033","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:04.058010","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:19.060117","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:19.060840","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:23.841428","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:01:23.841124+00:00","timestamp_unix":1771880483.841124}}}
|
||||
{"timestamp":"2026-02-23T23:01:23.841581","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:01:23.841150+00:00","timestamp_unix":1771880483.84115}}}
|
||||
{"timestamp":"2026-02-23T23:01:23.841676","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:01:23.841137+00:00","timestamp_unix":1771880483.841137}}}
|
||||
{"timestamp":"2026-02-23T23:01:23.841764","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:01:23.841066+00:00","timestamp_unix":1771880483.841066}}}
|
||||
{"timestamp":"2026-02-23T23:01:23.841933","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:01:23.841109+00:00","timestamp_unix":1771880483.841109}}}
|
||||
{"timestamp":"2026-02-23T23:01:23.873224","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:01:23.842163+00:00","validation_timestamp_unix":1771880483.842163,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:23.841124+00:00","timestamp_unix":1771880483.841124},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:01:23.841150+00:00","timestamp_unix":1771880483.84115},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:23.841137+00:00","timestamp_unix":1771880483.841137},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:23.841066+00:00","timestamp_unix":1771880483.841066},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:23.841109+00:00","timestamp_unix":1771880483.841109}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:01:34.063937","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:34.066277","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:49.072376","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:49.073165","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:01:53.842837","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:01:53.842430+00:00","timestamp_unix":1771880513.84243}}}
|
||||
{"timestamp":"2026-02-23T23:01:53.842990","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:01:53.842457+00:00","timestamp_unix":1771880513.842457}}}
|
||||
{"timestamp":"2026-02-23T23:01:53.843085","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:01:53.842443+00:00","timestamp_unix":1771880513.842443}}}
|
||||
{"timestamp":"2026-02-23T23:01:53.843172","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:01:53.842365+00:00","timestamp_unix":1771880513.842365}}}
|
||||
{"timestamp":"2026-02-23T23:01:53.843255","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:01:53.842415+00:00","timestamp_unix":1771880513.842415}}}
|
||||
{"timestamp":"2026-02-23T23:01:53.870117","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:01:53.843422+00:00","validation_timestamp_unix":1771880513.843422,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:53.842430+00:00","timestamp_unix":1771880513.84243},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:01:53.842457+00:00","timestamp_unix":1771880513.842457},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:53.842443+00:00","timestamp_unix":1771880513.842443},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:53.842365+00:00","timestamp_unix":1771880513.842365},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:01:53.842415+00:00","timestamp_unix":1771880513.842415}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:02:04.075979","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:04.078357","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:19.079392","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:19.082548","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:23.843784","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:02:23.842952+00:00","timestamp_unix":1771880543.842952}}}
|
||||
{"timestamp":"2026-02-23T23:02:23.844125","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:02:23.842985+00:00","timestamp_unix":1771880543.842985}}}
|
||||
{"timestamp":"2026-02-23T23:02:23.844316","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:02:23.842969+00:00","timestamp_unix":1771880543.842969}}}
|
||||
{"timestamp":"2026-02-23T23:02:23.844434","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:02:23.842839+00:00","timestamp_unix":1771880543.842839}}}
|
||||
{"timestamp":"2026-02-23T23:02:23.844528","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:02:23.842933+00:00","timestamp_unix":1771880543.842933}}}
|
||||
{"timestamp":"2026-02-23T23:02:23.878562","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:02:23.846489+00:00","validation_timestamp_unix":1771880543.846489,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:23.842952+00:00","timestamp_unix":1771880543.842952},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:02:23.842985+00:00","timestamp_unix":1771880543.842985},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:23.842969+00:00","timestamp_unix":1771880543.842969},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:23.842839+00:00","timestamp_unix":1771880543.842839},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:23.842933+00:00","timestamp_unix":1771880543.842933}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:02:34.082991","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:34.085161","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:49.085858","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:49.087763","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:02:53.844396","level":"INFO","source":"tm_ais","message":"Injected position","data":{"position":{"latitude":36.110657,"longitude":22.572672,"//timestamp_unix":1732461600.0,"source":"tm_ais","timestamp":"2026-02-23T21:02:53.843820+00:00","timestamp_unix":1771880573.84382}}}
|
||||
{"timestamp":"2026-02-23T23:02:53.844646","level":"INFO","source":"starlink_location","message":"Injected position","data":{"position":{"latitude":36.11055187009735,"longitude":22.57289484169309,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"source":"starlink_location","timestamp":"2026-02-23T21:02:53.843848+00:00","timestamp_unix":1771880573.843848}}}
|
||||
{"timestamp":"2026-02-23T23:02:53.844813","level":"INFO","source":"starlink_gps","message":"Injected position","data":{"position":{"latitude":36.11055287599966,"longitude":22.57289200819445,"//timestamp_unix":1732461600.0,"altitude":54.29000515150101,"source":"starlink_gps","timestamp":"2026-02-23T21:02:53.843834+00:00","timestamp_unix":1771880573.843834}}}
|
||||
{"timestamp":"2026-02-23T23:02:53.844998","level":"INFO","source":"nmea_primary","message":"Injected position","data":{"position":{"latitude":36.11063,"longitude":22.972875,"//timestamp_unix":1768308542.0,"altitude":14.0,"source":"nmea_primary","timestamp":"2026-02-23T21:02:53.843722+00:00","timestamp_unix":1771880573.843722}}}
|
||||
{"timestamp":"2026-02-23T23:02:53.845118","level":"INFO","source":"nmea_secondary","message":"Injected position","data":{"position":{"latitude":36.11085833333333,"longitude":22.572023333333334,"//timestamp_unix":1732461600.0,"altitude":13.2,"source":"nmea_secondary","timestamp":"2026-02-23T21:02:53.843800+00:00","timestamp_unix":1771880573.8438}}}
|
||||
{"timestamp":"2026-02-23T23:02:53.885574","level":"WARNING","source":"validation","message":"Validation failed","data":{"validation":{"validation_timestamp":"2026-02-23T21:02:53.846107+00:00","validation_timestamp_unix":1771880573.846107,"is_valid":false,"sources_missing":[],"sources_stale":[],"coordinate_differences":{"tm_ais_starlink_location":{"distance_meters":23.1816563266951,"source1":"tm_ais","source2":"starlink_location"},"tm_ais_starlink_gps":{"distance_meters":22.905468559001534,"source1":"tm_ais","source2":"starlink_gps"},"tm_ais_nmea_primary":{"distance_meters":35951.09222409758,"source1":"tm_ais","source2":"nmea_primary"},"tm_ais_nmea_secondary":{"distance_meters":62.42360552774925,"source1":"tm_ais","source2":"nmea_secondary"},"starlink_location_starlink_gps":{"distance_meters":0.27803087725616366,"source1":"starlink_location","source2":"starlink_gps"},"starlink_location_nmea_primary":{"distance_meters":35931.09887839561,"source1":"starlink_location","source2":"nmea_primary"},"starlink_location_nmea_secondary":{"distance_meters":85.38437464525116,"source1":"starlink_location","source2":"nmea_secondary"},"starlink_gps_nmea_primary":{"distance_meters":35931.353160486055,"source1":"starlink_gps","source2":"nmea_primary"},"starlink_gps_nmea_secondary":{"distance_meters":85.10634545958962,"source1":"starlink_gps","source2":"nmea_secondary"},"nmea_primary_nmea_secondary":{"distance_meters":36009.32592871677,"source1":"nmea_primary","source2":"nmea_secondary"}},"source_coordinates":{"tm_ais":{"latitude":36.110657,"longitude":22.572672,"altitude":null,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:53.843820+00:00","timestamp_unix":1771880573.84382},"starlink_location":{"latitude":36.11055187009735,"longitude":22.57289484169309,"altitude":54.29000515150101,"position_uncertainty_m":2.5,"timestamp":"2026-02-23T21:02:53.843848+00:00","timestamp_unix":1771880573.843848},"starlink_gps":{"latitude":36.11055287599966,"longitude":22.57289200819445,"altitude":54.29000515150101,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:53.843834+00:00","timestamp_unix":1771880573.843834},"nmea_primary":{"latitude":36.11063,"longitude":22.972875,"altitude":14.0,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:53.843722+00:00","timestamp_unix":1771880573.843722},"nmea_secondary":{"latitude":36.11085833333333,"longitude":22.572023333333334,"altitude":13.2,"position_uncertainty_m":null,"timestamp":"2026-02-23T21:02:53.843800+00:00","timestamp_unix":1771880573.8438}},"validation_details":{"threshold_meters":200.0,"stale_threshold_seconds":60,"expected_sources":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_found":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_with_coordinates":["tm_ais","starlink_location","starlink_gps","nmea_primary","nmea_secondary"],"sources_null_island":[],"max_distance_meters":36009.32592871677,"position_uncertainties":{"starlink_location":2.5}}}}}
|
||||
{"timestamp":"2026-02-23T23:03:04.088684","level":"ERROR","source":"nmea_primary","message":"Connection timeout for device nmea_primary","data":{"device_ip":"10.130.60.61","device_port":4001,"timeout":10}}
|
||||
{"timestamp":"2026-02-23T23:03:04.090180","level":"ERROR","source":"nmea_secondary","message":"Connection timeout for device nmea_secondary","data":{"device_ip":"10.130.60.61","device_port":4002,"timeout":10}}
|
||||
@@ -13,9 +13,19 @@ Revisions are tracked project-wide; see repo root **README.md** and `scripts/bum
|
||||
emmc-provisioning/
|
||||
├── README.md ← You are here
|
||||
├── docs/ Documentation
|
||||
│ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage
|
||||
│ ├── NETWORK-BOOT-LXC.md Network boot (PXE/dnsmasq) and LXC
|
||||
│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup
|
||||
│ ├── DEPLOY-NEW-PROXMOX.md ★ START HERE: full deploy guide (host prep, LXC, scripts, network boot)
|
||||
│ ├── EMMC-PROVISIONING-GUIDE.md Golden image creation, cloud-init, PiShrink
|
||||
│ ├── PROXMOX-LXC-DEPLOYMENT.md Reference: what is deployed, redeploy, troubleshooting
|
||||
│ ├── NETWORK-BOOT-LXC.md Network boot architecture (PXE/dnsmasq, interfaces)
|
||||
│ ├── NETWORK-BOOT-DEPLOYMENT-FLOW.md Full data flow for network boot provisioning
|
||||
│ ├── NETWORK-BOOT-TROUBLESHOOTING.md Troubleshooting network boot issues
|
||||
│ ├── DEVICE-DNS-DHCP-RESOLVCONF.md Device DNS from DHCP, resolv.conf, cloud-init
|
||||
│ ├── DNSMASQ-DNS-FILESERVER.md dnsmasq DNS and file.server on LXC
|
||||
│ ├── PROXMOX-HOST-COMPARISON.md Diff between Proxmox hosts (fixes checklist)
|
||||
│ ├── PREPARE-IMAGE-FOR-CLOUDINIT.md How to shrink and prep a golden image
|
||||
│ ├── BACKUP-DEVICE-CONFIG-AUDIT.md Audit of backup image contents
|
||||
│ ├── DEVICE-REMOVABLE-PACKAGES.md Packages to purge from the device image
|
||||
│ ├── EDIT-CLOUDINIT-ON-DEVICE.md Edit NoCloud files on-device or in .img.xz
|
||||
│ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference
|
||||
├── host/ Scripts that run on the provisioning host (Proxmox host)
|
||||
│ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd
|
||||
@@ -52,7 +62,8 @@ emmc-provisioning/
|
||||
|
||||
## Quick start
|
||||
|
||||
1. **Read** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for setup and usage.
|
||||
2. **Proxmox:** Use [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) to deploy to a Proxmox host; see [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md).
|
||||
3. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide).
|
||||
4. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**.
|
||||
1. **New deployment:** Follow **[docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md)** — covers Proxmox host prep, LXC creation, host scripts, network boot, and portal file sync.
|
||||
2. **Full setup reference:** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for golden image creation, cloud-init, PiShrink.
|
||||
3. **Redeploy / update:** Re-run `scripts/deploy-to-proxmox.sh root@HOST` — updates scripts, dashboard, udev, and systemd without touching your golden image or enabled flag.
|
||||
4. **Sync portal files:** After deploy or when kiosk/first-boot assets change: `scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>`.
|
||||
5. **Troubleshooting:** [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md) for USB errors, rpiboot failures, and monitoring. [docs/NETWORK-BOOT-TROUBLESHOOTING.md](docs/NETWORK-BOOT-TROUBLESHOOTING.md) for network boot issues.
|
||||
|
||||
@@ -1,4 +1,129 @@
|
||||
#!/bin/bash
|
||||
# Minimal bootstrap script for cloud-init first boot (test).
|
||||
set -e
|
||||
|
||||
# Ensure hostname resolves (avoids "sudo: unable to resolve host")
|
||||
H="$(hostname)"
|
||||
grep -q "127.0.1.1.*$H" /etc/hosts || echo "127.0.1.1 $H" >> /etc/hosts
|
||||
|
||||
# Do not overwrite /etc/resolv.conf: use DNS from DHCP so file.server and LXC DNS work.
|
||||
|
||||
# --- Chromium kiosk autostart (same behaviour as gnss-guard start-chromium.sh) ---
|
||||
PI_USER="${PI_USER:-pi}"
|
||||
SCRIPT_DEST="/usr/local/bin/start-chromium.sh"
|
||||
AUTOSTART_SYSTEM="/etc/xdg/autostart"
|
||||
PI_HOME="/home/$PI_USER"
|
||||
# Icon: download start-here.png from file server, or set DESKTOP_ICON to override
|
||||
FILE_SERVER="${FILE_SERVER:-http://file.server:5000/files/first-boot}"
|
||||
ICON_DEST="/usr/share/pixmaps/tm.png"
|
||||
DESKTOP_ICON="${DESKTOP_ICON:-chromium-browser}"
|
||||
if [ "$DESKTOP_ICON" = "chromium-browser" ]; then
|
||||
mkdir -p /usr/share/pixmaps
|
||||
icon_url="${FILE_SERVER}/start-here.png"
|
||||
if ! curl -fsSL "$icon_url" -o "$ICON_DEST" 2>/dev/null; then
|
||||
# Fallback: use gateway IP (LXC on provisioning LAN) when DNS not ready yet at first boot
|
||||
gw="$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')"
|
||||
if [ -n "$gw" ]; then
|
||||
curl -fsSL "http://${gw}:5000/files/first-boot/start-here.png" -o "$ICON_DEST" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
if [ -s "$ICON_DEST" ]; then
|
||||
chmod 644 "$ICON_DEST"
|
||||
DESKTOP_ICON="$ICON_DEST"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install start-chromium.sh system-wide so it works regardless of user home
|
||||
cat << 'START_CHROMIUM_EOF' > "$SCRIPT_DEST"
|
||||
#!/bin/bash
|
||||
# Disable keyring prompts
|
||||
export GNOME_KEYRING_CONTROL=""
|
||||
export DISPLAY=:0
|
||||
|
||||
# Force X11 instead of Wayland for better fullscreen support
|
||||
export GDK_BACKEND=x11
|
||||
unset WAYLAND_DISPLAY
|
||||
|
||||
# Wait for display and desktop environment to be ready
|
||||
for i in {1..60}; do
|
||||
if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then
|
||||
if pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1 || pgrep -x xfdesktop >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
sleep 5
|
||||
|
||||
/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 &
|
||||
|
||||
sleep 3
|
||||
for i in {1..10}; do
|
||||
WINDOW_ID=$(wmctrl -l 2>/dev/null | grep -i chromium | head -1 | awk '{print $1}')
|
||||
if [ -n "$WINDOW_ID" ]; then
|
||||
wmctrl -i -r "$WINDOW_ID" -b add,fullscreen 2>/dev/null
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
wait
|
||||
START_CHROMIUM_EOF
|
||||
chmod 755 "$SCRIPT_DEST"
|
||||
|
||||
# Autostart entry (runs Chromium at desktop login)
|
||||
mkdir -p "$AUTOSTART_SYSTEM"
|
||||
cat > "$AUTOSTART_SYSTEM/chromium-kiosk.desktop" << DESKTOP_EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Chromium Fullscreen
|
||||
Exec=/usr/local/bin/start-chromium.sh
|
||||
Icon=$DESKTOP_ICON
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
DESKTOP_EOF
|
||||
chmod 644 "$AUTOSTART_SYSTEM/chromium-kiosk.desktop"
|
||||
|
||||
# Desktop shortcut: real .desktop file on Desktop so the file manager treats it as a launcher (not a script).
|
||||
# Symlink with no extension was shown as "executable script" and prompted; .desktop runs directly with quick_exec=1.
|
||||
if getent passwd "$PI_USER" >/dev/null 2>&1; then
|
||||
mkdir -p "$PI_HOME/Desktop" "$PI_HOME/.config/libfm"
|
||||
if [ -f "$PI_HOME/.config/libfm/libfm.conf" ] && grep -q '^quick_exec=' "$PI_HOME/.config/libfm/libfm.conf"; then
|
||||
sed -i 's/^quick_exec=.*/quick_exec=1/' "$PI_HOME/.config/libfm/libfm.conf"
|
||||
else
|
||||
echo 'quick_exec=1' >> "$PI_HOME/.config/libfm/libfm.conf"
|
||||
fi
|
||||
chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config/libfm" 2>/dev/null || true
|
||||
DESKTOP_FILE="$PI_HOME/Desktop/GNSS Guard.desktop"
|
||||
cat > "$DESKTOP_FILE" << DESKTOP_SHORTCUT_EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GNSS Guard
|
||||
Comment=GNSS Guard Dashboard (e.g. if closed)
|
||||
Exec=/usr/local/bin/start-chromium.sh
|
||||
Icon=$DESKTOP_ICON
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
DESKTOP_SHORTCUT_EOF
|
||||
chmod 644 "$DESKTOP_FILE"
|
||||
chown "$PI_USER:$PI_USER" "$DESKTOP_FILE"
|
||||
# Remove old symlink if present
|
||||
rm -f "$PI_HOME/Desktop/GNSS Guard"
|
||||
# Application menu/panel entry (same content)
|
||||
SHORTCUT_FILE="/usr/share/applications/gnss-guard.desktop"
|
||||
cat > "$SHORTCUT_FILE" << DESKTOP_SHORTCUT_EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GNSS Guard
|
||||
Comment=GNSS Guard Dashboard (e.g. if closed)
|
||||
Exec=/usr/local/bin/start-chromium.sh
|
||||
Icon=$DESKTOP_ICON
|
||||
Terminal=true
|
||||
Categories=Utility;
|
||||
DESKTOP_SHORTCUT_EOF
|
||||
chmod 644 "$SHORTCUT_FILE"
|
||||
fi
|
||||
|
||||
echo "[$(date -Iseconds)] test completed" | tee -a /var/log/cloud-init-bootstrap.log
|
||||
|
||||
84
emmc-provisioning/cloud-init/fileserver/brightness-api.py
Normal file
84
emmc-provisioning/cloud-init/fileserver/brightness-api.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local HTTP API for screen-brightness override (crew manual control per MSC.191(79)).
|
||||
Listens on 127.0.0.1 only. Run as root so it can write /run/screen-brightness/override.
|
||||
"""
|
||||
import os
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
OVERRIDE_FILE = Path("/run/screen-brightness/override")
|
||||
RUN_DIR = OVERRIDE_FILE.parent
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 8765
|
||||
|
||||
|
||||
class BrightnessHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
path = self.path.split("?")[0]
|
||||
if path != "/brightness" and path != "/":
|
||||
self.send_error(404)
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length).decode("utf-8", errors="ignore") if length else ""
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
level = (query.get("level") or [None])[0] or (urllib.parse.parse_qs(body).get("level") or [None])[0]
|
||||
if not level:
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"missing level=1|2|3|4|5|auto")
|
||||
return
|
||||
level = level.strip().lower()
|
||||
if level == "auto":
|
||||
value = "auto"
|
||||
elif level in ("1", "2", "3", "4", "5"):
|
||||
value = level
|
||||
else:
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"level must be 1, 2, 3, 4, 5, or auto")
|
||||
return
|
||||
try:
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
OVERRIDE_FILE.write_text(value + "\n")
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(f"write failed: {e}".encode())
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(f"ok {value}\n".encode())
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.split("?")[0] in ("/", "/brightness", "/state"):
|
||||
state_file = Path("/run/screen-brightness/state")
|
||||
if state_file.exists():
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(state_file.read_bytes())
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # quiet; use journal if needed
|
||||
|
||||
|
||||
def main():
|
||||
if os.geteuid() != 0:
|
||||
raise SystemExit("brightness-api must run as root to write /run/screen-brightness/override")
|
||||
server = HTTPServer((HOST, PORT), BrightnessHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Local API for screen brightness override (crew manual control)
|
||||
Documentation=file:///usr/local/share/doc/screen-brightness/SCREEN-BRIGHTNESS-MANUAL-SETUP.md
|
||||
After=network.target
|
||||
Before=lightdm.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/brightness-api.py
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
User=root
|
||||
# Listen on 127.0.0.1 only
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Start the brightness overlay after a short delay so the Wayland/X session
|
||||
# and compositor are ready (autostart often runs before the display is usable).
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
sleep 6
|
||||
exec "$SCRIPT_DIR/brightness-overlay.py"
|
||||
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Brightness overlay
|
||||
Comment=Overlay button to control screen brightness (1-5, Auto)
|
||||
Exec=/home/pi/brightness-overlay-launch.sh
|
||||
Hidden=false
|
||||
NoDisplay=true
|
||||
X-GNOME-Autostart-enabled=true
|
||||
302
emmc-provisioning/cloud-init/fileserver/brightness-overlay.py
Normal file
302
emmc-provisioning/cloud-init/fileserver/brightness-overlay.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Brightness control overlay — always-on-top icon in bottom-right; click to show controls.
|
||||
Uses Wayland layer-shell OVERLAY so it stays above Chromium fullscreen/kiosk.
|
||||
Requires: brightness-api.service running (listens 127.0.0.1:8765).
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
LOG_TAG = "brightness-overlay"
|
||||
logging.basicConfig(
|
||||
stream=sys.stderr,
|
||||
level=logging.INFO,
|
||||
format=f"{LOG_TAG}: %(message)s",
|
||||
)
|
||||
log = logging.getLogger(LOG_TAG)
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk, GLib
|
||||
except Exception as e:
|
||||
log.error("need PyGObject Gtk: %s", e)
|
||||
sys.exit(1)
|
||||
|
||||
HAS_LAYER_SHELL = False
|
||||
try:
|
||||
gi.require_version("GtkLayerShell", "0.1")
|
||||
from gi.repository import GtkLayerShell
|
||||
HAS_LAYER_SHELL = True
|
||||
except Exception:
|
||||
log.warning("GtkLayerShell not available, using regular window")
|
||||
|
||||
API_URL = "http://127.0.0.1:8765/brightness"
|
||||
API_STATE_URL = "http://127.0.0.1:8765/state"
|
||||
|
||||
# Sizes
|
||||
ICON_SIZE = 44
|
||||
EXPANDED_WIDTH = 300
|
||||
PANEL_HEIGHT = 48
|
||||
|
||||
|
||||
def get_current_state():
|
||||
"""Return (level 1–5, is_auto). On error return (None, True) so Auto appears active."""
|
||||
try:
|
||||
with urllib.request.urlopen(API_STATE_URL, timeout=1) as r:
|
||||
text = r.read().decode()
|
||||
level = None
|
||||
override = "0"
|
||||
for line in text.strip().splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("level="):
|
||||
level = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("override="):
|
||||
override = line.split("=", 1)[1].strip()
|
||||
if level and level.isdigit() and 1 <= int(level) <= 5:
|
||||
return int(level), override == "0"
|
||||
return (None, True)
|
||||
except Exception:
|
||||
return (None, True)
|
||||
|
||||
|
||||
def set_brightness(level):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
API_URL,
|
||||
data=f"level={level}".encode(),
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=2) as r:
|
||||
log.info("brightness %s: %s", level, r.read().decode().strip())
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed (is brightness-api.service running?): %s", e)
|
||||
except Exception as e:
|
||||
log.warning("set_brightness failed: %s", e)
|
||||
|
||||
|
||||
def update_active_buttons(level_buttons, auto_btn, current_level, is_auto):
|
||||
"""Set .active style on the button that matches current state."""
|
||||
for i, btn in enumerate(level_buttons):
|
||||
ctx = btn.get_style_context()
|
||||
if not is_auto and current_level == i + 1:
|
||||
ctx.add_class("active")
|
||||
else:
|
||||
ctx.remove_class("active")
|
||||
if auto_btn:
|
||||
ctx = auto_btn.get_style_context()
|
||||
if is_auto:
|
||||
ctx.add_class("active")
|
||||
else:
|
||||
ctx.remove_class("active")
|
||||
|
||||
|
||||
def make_window_layer_shell(win):
|
||||
GtkLayerShell.init_for_window(win)
|
||||
GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY)
|
||||
# Anchor only to bottom and right so the surface sits in the bottom-right corner
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, False)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.LEFT, False)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.BOTTOM, True)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True)
|
||||
GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE)
|
||||
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.BOTTOM, 12)
|
||||
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.RIGHT, 12)
|
||||
if hasattr(GtkLayerShell, "set_namespace"):
|
||||
GtkLayerShell.set_namespace(win, "brightness-overlay")
|
||||
if hasattr(GtkLayerShell, "set_exclusive_zone"):
|
||||
GtkLayerShell.set_exclusive_zone(win, 0)
|
||||
log.info("layer-shell OVERLAY anchored bottom-right (always on top)")
|
||||
|
||||
|
||||
def position_fallback_bottom_right(win, width=None, height=None):
|
||||
"""Position window in bottom-right (X11: only works with toplevel, not POPUP)."""
|
||||
if width is None:
|
||||
width = ICON_SIZE
|
||||
if height is None:
|
||||
height = ICON_SIZE
|
||||
display = Gdk.Display.get_default()
|
||||
if not display:
|
||||
return
|
||||
n = display.get_n_monitors()
|
||||
monitor = display.get_monitor(0) if n else None
|
||||
if not monitor:
|
||||
return
|
||||
geom = monitor.get_geometry()
|
||||
x = geom.x + geom.width - width - 12
|
||||
y = geom.y + geom.height - height - 12
|
||||
win.move(x, y)
|
||||
|
||||
|
||||
def make_window_fallback(win):
|
||||
# Toplevel (not POPUP) so we can position and have no decorations on X11
|
||||
win.set_keep_above(True)
|
||||
win.set_decorated(False)
|
||||
win.set_type_hint(Gdk.WindowTypeHint.UTILITY) # tool-style window, often undecorated
|
||||
# Position after map so WM doesn't override
|
||||
win.connect("map-event", lambda w, e: position_fallback_bottom_right(w))
|
||||
log.info("X11 fallback: keep-above, undecorated, bottom-right")
|
||||
|
||||
|
||||
def main():
|
||||
# Normal toplevel: on X11 we can position it and set_decorated(False) removes title bar.
|
||||
# (POPUP without parent cannot be positioned on X11 and gets "temporary window" / centering.)
|
||||
win = Gtk.Window()
|
||||
win.set_decorated(False)
|
||||
win.set_resizable(False)
|
||||
win.set_default_size(ICON_SIZE, ICON_SIZE)
|
||||
win.set_skip_taskbar_hint(True)
|
||||
win.set_skip_pager_hint(True)
|
||||
# Transparent window: need RGBA visual and app-paintable
|
||||
screen = Gdk.Screen.get_default()
|
||||
if screen and screen.get_rgba_visual():
|
||||
win.set_visual(screen.get_rgba_visual())
|
||||
win.set_app_paintable(True)
|
||||
|
||||
# CSS: transparent window and overlay; only subtle hover so icon stays visible
|
||||
style = """
|
||||
GtkWindow, window {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
.brightness-icon-btn {
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
.brightness-icon-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.brightness-panel {
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
.brightness-panel.expanded {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.brightness-panel button {
|
||||
min-width: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.brightness-panel button.level-btn.active {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
"""
|
||||
css = Gtk.CssProvider()
|
||||
css.load_from_data(style.encode())
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
css,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
# Main horizontal box: [revealer with controls] [icon button]
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
|
||||
# Left part: revealer with Brightness + 1–5 + Auto
|
||||
strip_box = Gtk.Box(spacing=6)
|
||||
strip_box.set_margin_start(8)
|
||||
strip_box.set_margin_top(6)
|
||||
strip_box.set_margin_bottom(6)
|
||||
strip_box.set_margin_end(4)
|
||||
strip_box.get_style_context().add_class("brightness-panel")
|
||||
|
||||
strip_box.pack_start(Gtk.Label(label="Brightness "), False, False, 0)
|
||||
level_buttons = []
|
||||
for i in range(1, 6):
|
||||
btn = Gtk.Button(label=str(i))
|
||||
btn.get_style_context().add_class("level-btn")
|
||||
btn.set_can_focus(False)
|
||||
strip_box.pack_start(btn, False, False, 0)
|
||||
level_buttons.append(btn)
|
||||
auto_btn = Gtk.Button(label="Auto")
|
||||
auto_btn.get_style_context().add_class("level-btn")
|
||||
auto_btn.set_can_focus(False)
|
||||
strip_box.pack_start(auto_btn, False, False, 0)
|
||||
|
||||
def refresh_active_state():
|
||||
level, is_auto = get_current_state()
|
||||
update_active_buttons(level_buttons, auto_btn, level or 3, is_auto)
|
||||
|
||||
def on_level_click(btn, level):
|
||||
set_brightness(level)
|
||||
# Optimistic update so active state shows immediately
|
||||
if level == "auto":
|
||||
update_active_buttons(level_buttons, auto_btn, 3, True)
|
||||
else:
|
||||
update_active_buttons(level_buttons, auto_btn, int(level), False)
|
||||
# Sync from API after a short delay (daemon updates state file periodically)
|
||||
def delayed_refresh():
|
||||
refresh_active_state()
|
||||
return False
|
||||
GLib.timeout_add(500, delayed_refresh)
|
||||
|
||||
for i, btn in enumerate(level_buttons):
|
||||
btn.connect("clicked", on_level_click, str(i + 1))
|
||||
auto_btn.connect("clicked", on_level_click, "auto")
|
||||
|
||||
revealer = Gtk.Revealer()
|
||||
revealer.set_reveal_child(False)
|
||||
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
|
||||
revealer.set_transition_duration(150)
|
||||
revealer.add(strip_box)
|
||||
|
||||
main_box.pack_start(revealer, False, False, 0)
|
||||
|
||||
# Right part: always-visible icon button (click toggles panel)
|
||||
try:
|
||||
icon_img = Gtk.Image.new_from_icon_name("display-brightness-symbolic", Gtk.IconSize.BUTTON)
|
||||
except Exception:
|
||||
icon_img = Gtk.Label(label="☀")
|
||||
icon_btn = Gtk.Button()
|
||||
icon_btn.get_style_context().add_class("brightness-icon-btn")
|
||||
icon_btn.set_can_focus(False)
|
||||
icon_btn.add(icon_img)
|
||||
icon_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
|
||||
expanded = [False] # use list so closure can mutate
|
||||
use_fallback = not (HAS_LAYER_SHELL and GtkLayerShell.is_supported())
|
||||
|
||||
def on_icon_clicked(btn):
|
||||
expanded[0] = not expanded[0]
|
||||
revealer.set_reveal_child(expanded[0])
|
||||
ctx = strip_box.get_style_context()
|
||||
if expanded[0]:
|
||||
ctx.add_class("expanded")
|
||||
GLib.idle_add(refresh_active_state)
|
||||
win.resize(EXPANDED_WIDTH, PANEL_HEIGHT)
|
||||
if use_fallback:
|
||||
position_fallback_bottom_right(win, EXPANDED_WIDTH, PANEL_HEIGHT)
|
||||
else:
|
||||
ctx.remove_class("expanded")
|
||||
win.resize(ICON_SIZE, ICON_SIZE)
|
||||
if use_fallback:
|
||||
position_fallback_bottom_right(win, ICON_SIZE, ICON_SIZE)
|
||||
|
||||
icon_btn.connect("clicked", on_icon_clicked)
|
||||
main_box.pack_end(icon_btn, False, False, 0)
|
||||
|
||||
win.add(main_box)
|
||||
|
||||
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
|
||||
make_window_layer_shell(win)
|
||||
else:
|
||||
make_window_fallback(win)
|
||||
|
||||
win.connect("destroy", Gtk.main_quit)
|
||||
win.show_all()
|
||||
GLib.idle_add(refresh_active_state)
|
||||
log.info("brightness icon visible (click to show controls)")
|
||||
Gtk.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# screen-brightness.conf — reTerminal DM Bridge Brightness Daemon
|
||||
#
|
||||
# Regulatory basis
|
||||
# IMO MSC.191(79) §8.1 : brightness adjustable, never extinguished
|
||||
# IMO MSC.302(87) : alarms = high luminance; night mode preserves night vision
|
||||
# IHO S-52 / IEC 62288 : NIGHT / DUSK / DAY operating modes
|
||||
#
|
||||
# Reload without restart: systemctl reload screen-brightness
|
||||
# Override from bridge console:
|
||||
# echo 3 | sudo tee /run/screen-brightness/override # fix at level 3
|
||||
# echo auto | sudo tee /run/screen-brightness/override # return to auto
|
||||
|
||||
[screen-brightness]
|
||||
|
||||
# ── Sysfs paths ──────────────────────────────────────────────────────────────
|
||||
backlight_path = /sys/class/backlight/lcd_backlight/brightness
|
||||
buzzer_path = /sys/class/leds/usr-buzzer/brightness
|
||||
lux_path = /sys/bus/iio/devices/iio:device0/in_illuminance_input
|
||||
|
||||
# ── Timing ───────────────────────────────────────────────────────────────────
|
||||
# How often the ambient sensor is read (seconds)
|
||||
poll_interval = 2
|
||||
|
||||
# How often the buzzer is checked (seconds). Keep below 1 so alarms are seen quickly.
|
||||
buzzer_poll = 0.5
|
||||
|
||||
# How long to hold maximum brightness after the buzzer stops (seconds)
|
||||
# IMO MSC.302(87): alarm attention must persist long enough for crew response
|
||||
buzzer_cooldown = 10
|
||||
|
||||
# ── Lux thresholds — IHO S-52 three-mode framework ──────────────────────────
|
||||
# Adjust these to match the actual lighting environment of your bridge.
|
||||
#
|
||||
# 0 — lux_night_max → NIGHT mode (very dark, night watch)
|
||||
# lux_night_max+1 — lux_dusk_max → DUSK mode (dim, dawn/dusk)
|
||||
# lux_dusk_max+1 — lux_day_dim_max → DAY-DIM
|
||||
# lux_day_dim_max+1 — lux_day_normal_max → DAY-NORMAL
|
||||
# > lux_day_normal_max → DAY-BRIGHT (direct sunlight / tropical day)
|
||||
|
||||
lux_night_max = 20
|
||||
lux_dusk_max = 200
|
||||
lux_day_dim_max = 500
|
||||
lux_day_normal_max = 1000
|
||||
|
||||
# ── Brightness levels per mode (1 = minimum, 5 = maximum) ───────────────────
|
||||
# Level 0 is NEVER used — IMO "not to extinction" principle.
|
||||
# Minimum must remain legible at the navigator's position.
|
||||
level_night = 1
|
||||
level_dusk = 2
|
||||
level_day_dim = 3
|
||||
level_day_normal = 4
|
||||
level_day_bright = 5
|
||||
|
||||
# Brightness forced during ALERT (buzzer ON) and COOLDOWN states.
|
||||
# IMO MSC.302(87): alarm visual indicators must be high luminous intensity.
|
||||
level_alert = 5
|
||||
|
||||
# ── Hysteresis ───────────────────────────────────────────────────────────────
|
||||
# Number of consecutive polls that must agree on a new ambient brightness
|
||||
# target before the change is committed. Prevents flickering when lux hovers
|
||||
# near a mode boundary (e.g. sunset making the sensor oscillate 18–22 lux).
|
||||
# At poll_interval=2s and hysteresis_polls=3, a level change takes max 6s.
|
||||
hysteresis_polls = 3
|
||||
334
emmc-provisioning/cloud-init/fileserver/screen-brightness.py
Normal file
334
emmc-provisioning/cloud-init/fileserver/screen-brightness.py
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
reTerminal DM — Bridge Screen Brightness Daemon
|
||||
================================================
|
||||
Regulatory alignment
|
||||
IMO MSC.191(79) §8.1 Display must be dimmable but NEVER extinguished
|
||||
§8.1.2 Navigator must be able to reset to a preset level
|
||||
IMO MSC.302(87) Alarm visuals = high luminance; night mode must not
|
||||
degrade crew night vision
|
||||
IHO S-52 / IEC 62288 Three operating modes: NIGHT · DUSK · DAY
|
||||
|
||||
Operating states
|
||||
NIGHT Very low lux → level 1 (minimum — "not to extinction")
|
||||
DUSK Low lux → level 2 (dim)
|
||||
DAY-* Daylight → levels 3-5 graduated by lux sub-range
|
||||
ALERT Buzzer ON → level 5 (max) immediately — no smooth stepping
|
||||
COOLDOWN Buzzer just → hold level 5 for BUZZER_COOLDOWN_SEC seconds,
|
||||
stopped then fade back one step per poll
|
||||
OVERRIDE Manual file → fixed level set by navigator (per MSC.191(79))
|
||||
|
||||
Sysfs paths (reTerminal DM)
|
||||
Backlight : /sys/class/backlight/lcd_backlight/brightness (0-5)
|
||||
Buzzer : /sys/class/leds/usr-buzzer/brightness (0..max, non-zero = ON; often 255)
|
||||
Light IIO : /sys/bus/iio/devices/iio:device0/in_illuminance_input (lux)
|
||||
|
||||
Navigator override (satisfies MSC.191(79) §8.1.2 preset requirement)
|
||||
echo 3 > /run/screen-brightness/override # force fixed level 3
|
||||
echo auto > /run/screen-brightness/override # return to ambient control
|
||||
|
||||
Read current state (useful for dashboard integration)
|
||||
cat /run/screen-brightness/state
|
||||
|
||||
Config file : /etc/screen-brightness.conf (INI, optional)
|
||||
Reload config: kill -HUP <pid> (or: systemctl reload screen-brightness)
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# ── Default configuration ────────────────────────────────────────────────────
|
||||
DEFAULTS: dict = {
|
||||
# sysfs paths
|
||||
"backlight_path": "/sys/class/backlight/lcd_backlight/brightness",
|
||||
"buzzer_path": "/sys/class/leds/usr-buzzer/brightness",
|
||||
"lux_path": "/sys/bus/iio/devices/iio:device0/in_illuminance_input",
|
||||
|
||||
# timing
|
||||
"poll_interval": "2", # seconds between lux/ambient reads
|
||||
"buzzer_poll": "0.5", # seconds between buzzer checks (faster so we catch alarms)
|
||||
"buzzer_cooldown": "10", # seconds to hold max brightness after buzzer stops
|
||||
|
||||
# lux mode boundaries (IHO S-52 three-mode framework)
|
||||
"lux_night_max": "20", # 0–20 lux → NIGHT mode
|
||||
"lux_dusk_max": "200", # 21–200 lux → DUSK mode
|
||||
"lux_day_dim_max": "500", # 201–500 lux → DAY-DIM
|
||||
"lux_day_normal_max": "1000", # 501–1000 lux→ DAY-NORMAL
|
||||
# >1000 lux → DAY-BRIGHT
|
||||
|
||||
# backlight levels per mode (1 = minimum, 5 = maximum, 0 is NEVER used)
|
||||
"level_night": "1",
|
||||
"level_dusk": "2",
|
||||
"level_day_dim": "3",
|
||||
"level_day_normal": "4",
|
||||
"level_day_bright": "5",
|
||||
"level_alert": "5", # during ALERT and COOLDOWN states
|
||||
|
||||
# hysteresis: require this many consecutive polls agreeing on a new ambient
|
||||
# target before committing the change (prevents flickering near thresholds)
|
||||
"hysteresis_polls": "3",
|
||||
}
|
||||
|
||||
CONFIG_FILE = Path("/etc/screen-brightness.conf")
|
||||
OVERRIDE_FILE = Path("/run/screen-brightness/override")
|
||||
STATE_FILE = Path("/run/screen-brightness/state")
|
||||
RUN_DIR = Path("/run/screen-brightness")
|
||||
|
||||
# ── Logging — stdout (captured by journald) + syslog ────────────────────────
|
||||
log = logging.getLogger("screen-brightness")
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
_fmt_detail = logging.Formatter("%(asctime)s [screen-brightness] %(levelname)s %(message)s")
|
||||
_fmt_syslog = logging.Formatter("screen-brightness[%(process)d]: %(levelname)s %(message)s")
|
||||
|
||||
_stdout_h = logging.StreamHandler(sys.stdout)
|
||||
_stdout_h.setFormatter(_fmt_detail)
|
||||
log.addHandler(_stdout_h)
|
||||
|
||||
try:
|
||||
_syslog_h = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
_syslog_h.setFormatter(_fmt_syslog)
|
||||
log.addHandler(_syslog_h)
|
||||
except OSError:
|
||||
pass # /dev/log not available; journald captures stdout
|
||||
|
||||
|
||||
# ── Config helpers ───────────────────────────────────────────────────────────
|
||||
def load_config() -> dict:
|
||||
cfg = dict(DEFAULTS)
|
||||
if CONFIG_FILE.exists():
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(CONFIG_FILE)
|
||||
section = "screen-brightness"
|
||||
if parser.has_section(section):
|
||||
cfg.update(parser[section])
|
||||
log.info("Config loaded from %s", CONFIG_FILE)
|
||||
return cfg
|
||||
|
||||
|
||||
# ── Sysfs helpers ────────────────────────────────────────────────────────────
|
||||
def _read_int(path: Path, default: Optional[int] = None) -> Optional[int]:
|
||||
try:
|
||||
return int(path.read_text().strip())
|
||||
except Exception as exc:
|
||||
log.warning("Cannot read %s: %s", path, exc)
|
||||
return default
|
||||
|
||||
|
||||
def _write_int(path: Path, value: int) -> bool:
|
||||
try:
|
||||
path.write_text(f"{value}\n")
|
||||
return True
|
||||
except Exception as exc:
|
||||
log.error("Cannot write %s = %s: %s", path, value, exc)
|
||||
return False
|
||||
|
||||
|
||||
# ── Override file ────────────────────────────────────────────────────────────
|
||||
def read_override() -> Optional[int]:
|
||||
"""Return int level (1-5) if a manual override is active, else None."""
|
||||
try:
|
||||
if not OVERRIDE_FILE.exists():
|
||||
return None
|
||||
raw = OVERRIDE_FILE.read_text().strip().lower()
|
||||
if raw in ("auto", ""):
|
||||
return None
|
||||
val = int(raw)
|
||||
if 1 <= val <= 5:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── State file (read by dashboard / other services) ──────────────────────────
|
||||
def write_state(mode: str, level: int, lux: Optional[int],
|
||||
buzzer: bool, override: bool) -> None:
|
||||
try:
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(
|
||||
f"mode={mode}\n"
|
||||
f"level={level}\n"
|
||||
f"lux={lux if lux is not None else 'N/A'}\n"
|
||||
f"buzzer={'1' if buzzer else '0'}\n"
|
||||
f"override={'1' if override else '0'}\n"
|
||||
f"updated={int(time.time())}\n"
|
||||
)
|
||||
except Exception as exc:
|
||||
log.debug("Cannot write state file: %s", exc)
|
||||
|
||||
|
||||
# ── Lux → ambient level ───────────────────────────────────────────────────────
|
||||
def lux_to_level(lux: int, cfg: dict) -> Tuple[int, str]:
|
||||
"""Return (backlight_level, mode_name) for the given lux reading."""
|
||||
if lux <= int(cfg["lux_night_max"]):
|
||||
return int(cfg["level_night"]), "NIGHT"
|
||||
if lux <= int(cfg["lux_dusk_max"]):
|
||||
return int(cfg["level_dusk"]), "DUSK"
|
||||
if lux <= int(cfg["lux_day_dim_max"]):
|
||||
return int(cfg["level_day_dim"]), "DAY-DIM"
|
||||
if lux <= int(cfg["lux_day_normal_max"]):
|
||||
return int(cfg["level_day_normal"]), "DAY-NORMAL"
|
||||
return int(cfg["level_day_bright"]), "DAY-BRIGHT"
|
||||
|
||||
|
||||
# ── Main loop ────────────────────────────────────────────────────────────────
|
||||
def main() -> None:
|
||||
cfg = load_config()
|
||||
|
||||
BACKLIGHT = Path(cfg["backlight_path"])
|
||||
BUZZER = Path(cfg["buzzer_path"])
|
||||
LUX = Path(cfg["lux_path"])
|
||||
|
||||
for p in (BACKLIGHT, BUZZER, LUX):
|
||||
if not p.exists():
|
||||
log.warning("sysfs path not found at startup (will retry): %s", p)
|
||||
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# SIGHUP → reload config without restart (e.g. systemctl reload)
|
||||
_reload_flag = {"pending": False}
|
||||
|
||||
def _on_sighup(_sig: int, _frame: object) -> None:
|
||||
_reload_flag["pending"] = True
|
||||
|
||||
signal.signal(signal.SIGHUP, _on_sighup)
|
||||
|
||||
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
|
||||
poll_interval = float(cfg["poll_interval"])
|
||||
|
||||
log.info(
|
||||
"Bridge brightness daemon started | "
|
||||
"buzzer_poll=%ss lux_poll=%ss cooldown=%ss night≤%slux dusk≤%slux",
|
||||
buzz_interval, poll_interval, cfg["buzzer_cooldown"],
|
||||
cfg["lux_night_max"], cfg["lux_dusk_max"],
|
||||
)
|
||||
|
||||
current_level: int = -1 # last level written to sysfs
|
||||
cooldown_until: float = 0.0 # monotonic time when ALERT cooldown ends
|
||||
buzzer_was_on: bool = False
|
||||
|
||||
# Hysteresis state — track how many consecutive polls agree on a new ambient target
|
||||
pending_ambient: int = -1
|
||||
pending_count: int = 0
|
||||
|
||||
# Buzzer is checked every buzzer_poll (0.5s); lux/ambient only every poll_interval (2s)
|
||||
last_lux_poll: float = -999.0 # force first lux read
|
||||
lux: Optional[int] = None # last successful lux read (reused between lux polls)
|
||||
target, mode = 1, "NIGHT" # fallback for step calc before first ambient target
|
||||
|
||||
while True:
|
||||
# ── Config reload on SIGHUP ──────────────────────────────────────────
|
||||
if _reload_flag["pending"]:
|
||||
cfg = load_config()
|
||||
BACKLIGHT = Path(cfg["backlight_path"])
|
||||
BUZZER = Path(cfg["buzzer_path"])
|
||||
LUX = Path(cfg["lux_path"])
|
||||
_reload_flag["pending"] = False
|
||||
pending_ambient = -1
|
||||
pending_count = 0
|
||||
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
|
||||
poll_interval = float(cfg["poll_interval"])
|
||||
log.info("Config reloaded (SIGHUP)")
|
||||
|
||||
now = time.monotonic()
|
||||
buzzer_raw = _read_int(BUZZER, default=0) or 0
|
||||
buzzer = buzzer_raw > 0
|
||||
override = read_override()
|
||||
|
||||
# Read lux only every poll_interval seconds
|
||||
if now - last_lux_poll >= poll_interval:
|
||||
last_lux_poll = now
|
||||
lux = _read_int(LUX, default=None)
|
||||
|
||||
# ── Determine target level and mode name ─────────────────────────────
|
||||
immediate = False # skip smooth stepping when True
|
||||
|
||||
if override is not None:
|
||||
target, mode = override, "OVERRIDE"
|
||||
immediate = True
|
||||
|
||||
elif buzzer:
|
||||
target, mode = int(cfg["level_alert"]), "ALERT"
|
||||
cooldown_until = now + int(cfg["buzzer_cooldown"])
|
||||
immediate = True # alarm: go to max brightness instantly
|
||||
if not buzzer_was_on:
|
||||
log.info("ALERT: buzzer ON → brightness %d (immediate)", target)
|
||||
|
||||
elif now < cooldown_until:
|
||||
target, mode = int(cfg["level_alert"]), "COOLDOWN"
|
||||
immediate = True # hold alert level until cooldown expires
|
||||
if buzzer_was_on:
|
||||
remaining = cooldown_until - now
|
||||
log.info("COOLDOWN: buzzer OFF → hold brightness %d for %.0fs",
|
||||
target, remaining)
|
||||
|
||||
else:
|
||||
# Ambient mode — lux controls level (use last lux read)
|
||||
if lux is None:
|
||||
# Sensor not read yet or unreadable — keep current level
|
||||
buzzer_was_on = buzzer
|
||||
time.sleep(buzz_interval)
|
||||
continue
|
||||
|
||||
ambient_target, mode = lux_to_level(lux, cfg)
|
||||
|
||||
# Hysteresis: commit new ambient level only after N consecutive
|
||||
# polls agree (avoids flickering near a lux threshold)
|
||||
if ambient_target == pending_ambient:
|
||||
pending_count += 1
|
||||
else:
|
||||
pending_ambient = ambient_target
|
||||
pending_count = 1
|
||||
|
||||
if pending_count >= int(cfg["hysteresis_polls"]):
|
||||
target = ambient_target
|
||||
else:
|
||||
# Not stable yet — keep current level if we have one
|
||||
target = current_level if current_level > 0 else ambient_target
|
||||
|
||||
if buzzer_was_on and not buzzer and now >= cooldown_until:
|
||||
log.info("COOLDOWN done → %s | lux=%d | brightness=%d", mode, lux, target)
|
||||
|
||||
# ── Smooth stepping vs immediate ─────────────────────────────────────
|
||||
# Ambient changes step one level at a time to avoid harsh
|
||||
# brightness jumps that could disrupt night-adapted vision.
|
||||
# Alarm and override changes are immediate (safety requirement).
|
||||
if immediate or current_level < 0:
|
||||
step = target
|
||||
elif target > current_level:
|
||||
step = current_level + 1
|
||||
elif target < current_level:
|
||||
step = current_level - 1
|
||||
else:
|
||||
step = current_level # no change needed
|
||||
|
||||
# ── Write only when level actually changes ───────────────────────────
|
||||
if step != current_level:
|
||||
if _write_int(BACKLIGHT, step):
|
||||
log.info(
|
||||
"Brightness %s→%d | mode=%-10s | lux=%s",
|
||||
current_level if current_level >= 0 else "—",
|
||||
step, mode,
|
||||
lux if lux is not None else "N/A",
|
||||
)
|
||||
current_level = step
|
||||
|
||||
write_state(mode, current_level, lux, buzzer, override is not None)
|
||||
buzzer_was_on = buzzer
|
||||
time.sleep(buzz_interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
log.info("Stopped by keyboard interrupt.")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=reTerminal DM screen brightness daemon (ambient + buzzer)
|
||||
Documentation=https://wiki.seeedstudio.com/reterminal-dm/
|
||||
After=multi-user.target
|
||||
# Wait for the IIO light sensor and backlight sysfs nodes to be available
|
||||
After=systemd-udev-settle.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Ensure brightness starts in auto (ambient) mode on every boot
|
||||
ExecStartPre=/bin/sh -c 'mkdir -p /run/screen-brightness && echo auto > /run/screen-brightness/override'
|
||||
ExecStart=/usr/local/bin/screen-brightness.py
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
# Needs root to write to /sys/class/backlight and /sys/class/leds
|
||||
User=root
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Step 14: Install screen brightness daemon (ambient light + buzzer-triggered alert)
|
||||
# Regulatory: IMO MSC.191(79), MSC.302(87), IHO S-52 / IEC 62288
|
||||
|
||||
step_14_screen_brightness() {
|
||||
local script_dst="/usr/local/bin/screen-brightness.py"
|
||||
local service_dst="/etc/systemd/system/screen-brightness.service"
|
||||
local conf_dst="/etc/screen-brightness.conf"
|
||||
|
||||
log "Downloading screen-brightness.py ..."
|
||||
curl -fsSL "${FILE_SERVER}/screen-brightness.py" -o "$script_dst" \
|
||||
|| { log "ERROR: could not download screen-brightness.py"; return 1; }
|
||||
chmod 755 "$script_dst"
|
||||
log "Installed $script_dst"
|
||||
|
||||
log "Downloading screen-brightness.service ..."
|
||||
curl -fsSL "${FILE_SERVER}/screen-brightness.service" -o "$service_dst" \
|
||||
|| { log "ERROR: could not download screen-brightness.service"; return 1; }
|
||||
chmod 644 "$service_dst"
|
||||
log "Installed $service_dst"
|
||||
|
||||
# Deploy default config only if one doesn't already exist on the device
|
||||
# (preserves any vessel-specific tuning done post-provisioning)
|
||||
if [[ ! -f "$conf_dst" ]]; then
|
||||
log "Downloading screen-brightness.conf (default) ..."
|
||||
curl -fsSL "${FILE_SERVER}/screen-brightness.conf" -o "$conf_dst" \
|
||||
|| log "WARNING: could not download screen-brightness.conf (daemon uses built-in defaults)"
|
||||
[[ -f "$conf_dst" ]] && chmod 644 "$conf_dst" && log "Installed $conf_dst"
|
||||
else
|
||||
log "Config $conf_dst already exists — keeping vessel settings"
|
||||
fi
|
||||
|
||||
# Create the runtime dir; tmpfiles.d rule ensures it re-appears after reboot
|
||||
mkdir -p /run/screen-brightness
|
||||
cat > /etc/tmpfiles.d/screen-brightness.conf <<'TMPFILES'
|
||||
d /run/screen-brightness 0755 root root -
|
||||
TMPFILES
|
||||
|
||||
# Brightness API (localhost only) so overlay can set override without root
|
||||
log "Downloading brightness-api.py ..."
|
||||
curl -fsSL "${FILE_SERVER}/brightness-api.py" -o /usr/local/bin/brightness-api.py \
|
||||
&& chmod 755 /usr/local/bin/brightness-api.py \
|
||||
&& log "Installed /usr/local/bin/brightness-api.py" || true
|
||||
if curl -fsSL "${FILE_SERVER}/brightness-api.service" -o /etc/systemd/system/brightness-api.service 2>/dev/null; then
|
||||
chmod 644 /etc/systemd/system/brightness-api.service
|
||||
systemctl enable brightness-api.service
|
||||
log "brightness-api.service enabled"
|
||||
fi
|
||||
|
||||
# Brightness overlay (crew manual control on top of kiosk)
|
||||
if curl -fsSL "${FILE_SERVER}/brightness-overlay.py" -o "$PI_HOME/brightness-overlay.py" 2>/dev/null; then
|
||||
chmod 755 "$PI_HOME/brightness-overlay.py"
|
||||
chown "$PI_USER:$PI_USER" "$PI_HOME/brightness-overlay.py"
|
||||
if curl -fsSL "${FILE_SERVER}/brightness-overlay-launch.sh" -o "$PI_HOME/brightness-overlay-launch.sh" 2>/dev/null; then
|
||||
chmod 755 "$PI_HOME/brightness-overlay-launch.sh"
|
||||
chown "$PI_USER:$PI_USER" "$PI_HOME/brightness-overlay-launch.sh"
|
||||
fi
|
||||
if curl -fsSL "${FILE_SERVER}/brightness-overlay.desktop" -o /tmp/br-overlay.desktop 2>/dev/null; then
|
||||
sed "s|/home/pi|$PI_HOME|g" /tmp/br-overlay.desktop > "$AUTOSTART/brightness-overlay.desktop"
|
||||
chown "$PI_USER:$PI_USER" "$AUTOSTART/brightness-overlay.desktop"
|
||||
chmod 644 "$AUTOSTART/brightness-overlay.desktop"
|
||||
rm -f /tmp/br-overlay.desktop
|
||||
log "brightness-overlay installed (autostart)"
|
||||
fi
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable screen-brightness.service
|
||||
log "screen-brightness.service enabled (starts on next boot)"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test buzzer so screen-brightness daemon raises backlight (ALERT) and holds
|
||||
# during COOLDOWN. Holds buzzer ON for 3+ seconds so the daemon's 2s poll sees it.
|
||||
#
|
||||
# reTerminal DM: /sys/class/leds/usr-buzzer/brightness (0 = off, 1 = on)
|
||||
# Run as root: sudo ./test-buzzer-brightness.sh
|
||||
|
||||
set -e
|
||||
BUZZER="/sys/class/leds/usr-buzzer/brightness"
|
||||
MAX="/sys/class/leds/usr-buzzer/max_brightness"
|
||||
|
||||
if [[ ! -f "$BUZZER" ]]; then
|
||||
echo "ERROR: Buzzer not found at $BUZZER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "Run as root: sudo $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Quick check: is the daemon running? (so we know what to expect)
|
||||
echo "--- Pre-check ---"
|
||||
if systemctl is-active --quiet screen-brightness.service 2>/dev/null; then
|
||||
echo " screen-brightness.service: active"
|
||||
if [[ -f /run/screen-brightness/state ]]; then
|
||||
echo " Current state:"
|
||||
sed 's/^/ /' /run/screen-brightness/state
|
||||
else
|
||||
echo " (no /run/screen-brightness/state yet)"
|
||||
fi
|
||||
echo " Recent logs:"
|
||||
journalctl -u screen-brightness.service -n 3 --no-pager 2>/dev/null | sed 's/^/ /' || true
|
||||
else
|
||||
echo " screen-brightness.service: NOT ACTIVE — start it with: sudo systemctl start screen-brightness.service"
|
||||
echo " Brightness will not change until the daemon is running."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Hold buzzer ON for 3 seconds so the daemon's 2s poll interval will see it
|
||||
ON_VAL=255
|
||||
if [[ -f "$MAX" ]]; then
|
||||
ON_VAL="$(cat "$MAX" 2>/dev/null || echo 255)"
|
||||
fi
|
||||
|
||||
echo "Beeping: 3 short beeps, then buzzer ON for 3 s (watch brightness go to 5)..."
|
||||
for i in 1 2 3; do
|
||||
echo "$ON_VAL" > "$BUZZER"
|
||||
sleep 0.15
|
||||
echo 0 > "$BUZZER"
|
||||
sleep 0.25
|
||||
done
|
||||
echo "$ON_VAL" > "$BUZZER"
|
||||
sleep 3
|
||||
echo 0 > "$BUZZER"
|
||||
|
||||
echo "Buzzer off. Brightness should stay at 5 for ~10 s (cooldown), then return to ambient."
|
||||
echo ""
|
||||
echo "--- After (check state and logs) ---"
|
||||
if [[ -f /run/screen-brightness/state ]]; then
|
||||
echo " State:"
|
||||
sed 's/^/ /' /run/screen-brightness/state
|
||||
fi
|
||||
echo " Recent logs:"
|
||||
journalctl -u screen-brightness.service -n 8 --no-pager 2>/dev/null | sed 's/^/ /' || echo " (no logs or service not running)"
|
||||
@@ -48,7 +48,7 @@ if [[ -z "$FIRST_BOOT_CONF" ]]; then
|
||||
fi
|
||||
|
||||
# Step enable flags (default: all enabled)
|
||||
for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13; do
|
||||
for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14; do
|
||||
eval "ENABLE_STEP_${_n}=\"\${ENABLE_STEP_${_n}:-1}\""
|
||||
done
|
||||
|
||||
@@ -165,6 +165,7 @@ elif [[ "$CURRENT_PHASE" == "2" ]]; then
|
||||
run_step 10 cmdline
|
||||
run_step 11 oneshots
|
||||
run_step 12 log_permissions
|
||||
run_step 14 screen_brightness
|
||||
phase2_cleanup
|
||||
run_step 13 reboot
|
||||
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
# as first-boot.conf; then add a runcmd line to download it to /tmp/first-boot.conf before
|
||||
# running first-boot.sh so the script loads your config.
|
||||
# 4. To use a different username than "pi", set PI_USER in first-boot.conf and create that user below.
|
||||
# 5. DNS: manage_resolv_conf: false and NM rc-manager=symlink so the device uses DNS from DHCP
|
||||
# (LXC option 6) and file.server resolves. See docs/DEVICE-DNS-DHCP-RESOLVCONF.md.
|
||||
|
||||
package_update: true
|
||||
package_upgrade: false
|
||||
|
||||
# Do not overwrite /etc/resolv.conf; device will use DNS from DHCP (LXC sends option 6).
|
||||
manage_resolv_conf: false
|
||||
|
||||
packages:
|
||||
- curl
|
||||
|
||||
@@ -30,7 +35,17 @@ write_files:
|
||||
PasswordAuthentication yes
|
||||
PermitRootLogin no
|
||||
|
||||
# NetworkManager: manage resolv.conf via symlink so it gets DNS from DHCP (option 6 from LXC).
|
||||
- path: /etc/NetworkManager/conf.d/99-resolv-dhcp.conf
|
||||
content: |
|
||||
[main]
|
||||
rc-manager=symlink
|
||||
permissions: '0644'
|
||||
|
||||
runcmd:
|
||||
# Allow NM to manage resolv.conf with DHCP DNS (remove static file if present).
|
||||
- rm -f /etc/resolv.conf
|
||||
- systemctl restart NetworkManager || true
|
||||
- systemctl enable ssh
|
||||
- systemctl start ssh
|
||||
- curl -fsSL "http://10.20.50.1:5000/files/first-boot.sh" -o /tmp/first-boot.sh
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
# provisioning portal or file server). Example: http://10.20.50.1:5000/files/bootstrap.sh
|
||||
# 2. Copy this file to the boot partition as "user-data" (with meta-data and optional network-config).
|
||||
# 3. Edit BOOTSTRAP_URL below to match your server (or set it once in the runcmd section).
|
||||
#
|
||||
# DNS: Use NetworkManager rc-manager=symlink so /etc/resolv.conf gets DHCP DNS (LXC option 6).
|
||||
# RPi OS does not use systemd-resolved by default. Ensure bootstrap.sh does not overwrite
|
||||
# /etc/resolv.conf. See docs/DEVICE-DNS-DHCP-RESOLVCONF.md.
|
||||
|
||||
package_update: true
|
||||
package_upgrade: false
|
||||
|
||||
# Keep /etc/hosts in sync with hostname (from meta-data or set below)
|
||||
manage_etc_hosts: true
|
||||
|
||||
# Do not overwrite /etc/resolv.conf; NetworkManager will manage it with DHCP DNS
|
||||
manage_resolv_conf: false
|
||||
|
||||
packages:
|
||||
- curl
|
||||
|
||||
@@ -19,12 +29,23 @@ write_files:
|
||||
PasswordAuthentication yes
|
||||
PermitRootLogin no
|
||||
|
||||
# NetworkManager: manage resolv.conf via symlink so it gets DNS from DHCP (option 6 from LXC).
|
||||
# RPi OS does not use systemd-resolved; NM writes /etc/resolv.conf -> /run/NetworkManager/resolv.conf.
|
||||
- path: /etc/NetworkManager/conf.d/99-resolv-dhcp.conf
|
||||
content: |
|
||||
[main]
|
||||
rc-manager=symlink
|
||||
permissions: '0644'
|
||||
|
||||
runcmd:
|
||||
# Remove static resolv.conf so NM creates its symlink with DHCP DNS (file.server will resolve).
|
||||
- rm -f /etc/resolv.conf
|
||||
- systemctl restart NetworkManager || true
|
||||
- systemctl enable ssh
|
||||
- systemctl start ssh
|
||||
# Download and run bootstrap script (edit URL to match your file server)
|
||||
- |
|
||||
BOOTSTRAP_URL="http://10.20.50.1:5000/files/bootstrap.sh"
|
||||
BOOTSTRAP_URL="http://file.server:5000/files/bootstrap.sh"
|
||||
LOG="/var/log/cloud-init-bootstrap.log"
|
||||
if ! curl -fsSL "$BOOTSTRAP_URL" -o /tmp/bootstrap.sh 2>>"$LOG" || [ ! -s /tmp/bootstrap.sh ]; then
|
||||
echo "$(date -Iseconds) ERROR: Failed to download bootstrap.sh from $BOOTSTRAP_URL (file missing or empty)" >> "$LOG"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin · CM4 Provisioning</title>
|
||||
<title>Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -76,6 +76,23 @@
|
||||
<p id="goldenInfo" class="mono" style="font-size:0.9rem;">Loading…</p>
|
||||
</div>
|
||||
|
||||
<!-- Update boot (EEPROM) -->
|
||||
<div class="section" id="adminUpdateBootSection">
|
||||
<h2 class="section-title">Update boot (EEPROM)</h2>
|
||||
<p id="adminUpdateBootHint" class="mono" style="font-size:0.85rem; color:var(--text-muted);">When a device is connected in USB boot mode, it appears here. Use this to write EEPROM boot order (e.g. eMMC only) to the device.</p>
|
||||
<div id="adminUpdateBootDevice" style="display:none; margin-top:0.5rem;">
|
||||
<p class="mono" style="font-size:0.9rem; margin-bottom:0.5rem;">Device in USB boot mode</p>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
|
||||
<label>Boot order:</label>
|
||||
<select id="adminEepromPreset" class="eeprom-preset" title="Boot order">
|
||||
<option value="0x1">eMMC only</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="adminUpdateEepromBtn">Update EEPROM</button>
|
||||
</div>
|
||||
</div>
|
||||
<p id="adminUpdateBootNone" class="mono" style="font-size:0.85rem; color:var(--text-muted); display:none;">No device in USB boot mode. Connect a device with eMMC disable jumper and USB to host.</p>
|
||||
</div>
|
||||
|
||||
<!-- Backups -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
@@ -247,6 +264,47 @@
|
||||
authFetch('/api/admin/users', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username: username, password: password}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { document.getElementById('addUserForm').style.display = 'none'; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; fetchUsers(); } else alert(d.error); });
|
||||
};
|
||||
|
||||
function fetchPendingDevices() {
|
||||
authFetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){
|
||||
var devEl = document.getElementById('adminUpdateBootDevice');
|
||||
var noneEl = document.getElementById('adminUpdateBootNone');
|
||||
if (d.usb) {
|
||||
if (devEl) devEl.style.display = 'block';
|
||||
if (noneEl) noneEl.style.display = 'none';
|
||||
} else {
|
||||
if (devEl) devEl.style.display = 'none';
|
||||
if (noneEl) noneEl.style.display = 'block';
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
function fetchEepromPresets() {
|
||||
authFetch('/api/eeprom-presets').then(function(r){ return r.json(); }).then(function(d){
|
||||
var sel = document.getElementById('adminEepromPreset');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '';
|
||||
(d.presets || []).forEach(function(p){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = p.id || p.value;
|
||||
opt.textContent = p.label || p.id || p.value;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}).catch(function(){});
|
||||
}
|
||||
fetchPendingDevices();
|
||||
fetchEepromPresets();
|
||||
var adminUpdateEepromBtn = document.getElementById('adminUpdateEepromBtn');
|
||||
if (adminUpdateEepromBtn) {
|
||||
adminUpdateEepromBtn.onclick = function(){
|
||||
var presetEl = document.getElementById('adminEepromPreset');
|
||||
var bootOrder = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
authFetch('/api/device-action', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ source: 'usb', action: 'eeprom_update', boot_order: bootOrder }) })
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){ if (d.ok) { fetchPendingDevices(); alert('Update EEPROM sent. The host will write to the device.'); } else alert(d.error || 'Failed'); })
|
||||
.catch(function(){ alert('Request failed'); });
|
||||
};
|
||||
}
|
||||
setInterval(fetchPendingDevices, 3000);
|
||||
|
||||
fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden();
|
||||
setInterval(fetchLogs, 30000);
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cloud-init build · Admin · CM4 Provisioning</title>
|
||||
<title>Cloud-init build · Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Deploy · CM4 Provisioning</title>
|
||||
<title>Deploy · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -96,7 +96,7 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="header">
|
||||
<h1>CM4 eMMC Provisioning</h1>
|
||||
<h1>GNSS Guard Provisioning</h1>
|
||||
<a href="/admin">Admin</a>
|
||||
</header>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<h2 class="card-title">Status</h2>
|
||||
<div id="status" class="status-row">
|
||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for device</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
|
||||
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||
</div>
|
||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Capture or deploy</h2>
|
||||
<h2 class="card-title">Deploy and Backup</h2>
|
||||
<p id="shrinkOptionWrap" style="display:none; margin-bottom: 0.5rem; font-size: 0.8rem;"><label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label></p>
|
||||
<div id="pendingDevices"></div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p>
|
||||
@@ -142,12 +142,12 @@
|
||||
<p id="goldenInfo" class="golden-info">Loading…</p>
|
||||
</div>
|
||||
|
||||
<details class="card" style="padding: 0;">
|
||||
<details class="card" style="padding: 0;" open>
|
||||
<summary>Recent log</summary>
|
||||
<div class="inner"><pre id="log" class="log-pre"></pre></div>
|
||||
</details>
|
||||
|
||||
<details class="card" style="padding: 0; margin-top: 1rem;">
|
||||
<details class="card" style="padding: 0; margin-top: 1rem;" open>
|
||||
<summary>DHCP leases</summary>
|
||||
<div class="inner">
|
||||
<p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p>
|
||||
@@ -162,7 +162,7 @@
|
||||
<div class="inner">
|
||||
<p class="help-sub">USB boot</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper).</li>
|
||||
<li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper).</li>
|
||||
<li><span class="num">2</span> Connect USB to host; choose <strong>Backup</strong> or <strong>Deploy</strong> above.</li>
|
||||
<li><span class="num">3</span> Remove jumper and power cycle when done.</li>
|
||||
</ol>
|
||||
@@ -178,7 +178,7 @@
|
||||
const phase = data.phase || 'idle';
|
||||
document.getElementById('statusPill').className = 'status-pill ' + phase;
|
||||
document.getElementById('statusPill').textContent = phaseLabels[phase] || phase;
|
||||
document.getElementById('statusMsg').textContent = data.message || '';
|
||||
document.getElementById('statusMsg').textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
|
||||
const err = document.getElementById('statusErr');
|
||||
if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
|
||||
if (phase === 'done') scheduleDoneClear();
|
||||
@@ -204,7 +204,7 @@
|
||||
shrinkWrap.style.display = 'block';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-desc">USB device — choose Backup, Deploy, or Update EEPROM</div><div class="device-actions-row"><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button> <select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select> <button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>';
|
||||
el.innerHTML = '<div class="device-desc">USB device — choose Backup or Deploy</div><div class="device-actions-row"><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
} else shrinkWrap.style.display = 'none';
|
||||
noPending.style.display = hasAny ? 'none' : 'block';
|
||||
@@ -212,10 +212,6 @@
|
||||
btn.onclick = function() {
|
||||
const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
|
||||
if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
|
||||
if (body.action === 'eeprom_update' && body.source === 'usb') {
|
||||
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
|
||||
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
}
|
||||
if (body.action === 'backup' && document.getElementById('shrinkAfterBackup').checked) body.shrink = true;
|
||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -267,7 +263,14 @@
|
||||
fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){});
|
||||
}
|
||||
function fetchPending() { fetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){ renderPending(d.usb || null, d.network || []); }).catch(function(){ renderPending(null, []); }); }
|
||||
function fetchLog() { fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){ document.getElementById('log').textContent = d.log || ''; }).catch(function(){}); }
|
||||
function fetchLog() {
|
||||
fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){
|
||||
var logEl = document.getElementById('log');
|
||||
if (!logEl) return;
|
||||
logEl.textContent = d.log || '';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}).catch(function(){});
|
||||
}
|
||||
function fetchGolden() {
|
||||
fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
|
||||
const el = document.getElementById('goldenInfo');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CM4 eMMC Provisioning</title>
|
||||
<title>GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -359,8 +359,8 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="header">
|
||||
<h1>CM4 eMMC Provisioning</h1>
|
||||
<p>Deploy or backup reTerminal via USB boot mode</p>
|
||||
<h1>GNSS Guard Provisioning</h1>
|
||||
<p>Deploy or backup device via USB boot mode</p>
|
||||
</header>
|
||||
|
||||
<!-- 1. Current status -->
|
||||
@@ -368,12 +368,12 @@
|
||||
<h2 class="section-title">Current status</h2>
|
||||
<div id="status" class="status-row">
|
||||
<span id="statusPill" class="status-pill idle">Idle</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for device</span>
|
||||
<span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
|
||||
<button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
|
||||
</div>
|
||||
<div id="statusErr" class="status-err" style="display:none;"></div>
|
||||
<div id="statusGoldenHint" class="backup-deploy-hint" style="display:none; margin-top:0.75rem;">
|
||||
No golden image is required to <strong>capture</strong>. Connect a device in USB boot mode; when it appears under “Capture image or deploy”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
|
||||
No golden image is required to <strong>backup</strong>. Connect a device in USB boot mode; when it appears under “Deploy and Backup”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
|
||||
<button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button>
|
||||
</div>
|
||||
<div id="statusMeta" class="status-meta" style="display:none;"></div>
|
||||
@@ -384,8 +384,8 @@
|
||||
|
||||
<!-- 2. Capture (Backup) or Deploy -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Capture image or deploy</h2>
|
||||
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode. When the device appears below, click <strong>Backup</strong> to save its eMMC to a file. To write an image to a device, click <strong>Deploy</strong> (requires a golden image).</p>
|
||||
<h2 class="section-title">Deploy and Backup</h2>
|
||||
<p class="backup-deploy-hint">Connect a device in USB boot mode. When it appears below, click <strong>Backup</strong> to save its eMMC to a file, or <strong>Deploy</strong> to write the golden image to the device (requires a golden image set in Admin).</p>
|
||||
<p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;">
|
||||
<label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label> <span class="backups-mono" style="font-size:0.8rem;">(reduces size; requires PiShrink on host)</span>
|
||||
</p>
|
||||
@@ -398,7 +398,7 @@
|
||||
</span>
|
||||
<span>— USB boot mode</span>
|
||||
</div>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect reTerminal in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
|
||||
<p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect device in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Saved backups -->
|
||||
@@ -477,7 +477,7 @@
|
||||
<div class="inner">
|
||||
<p class="help-sub">USB boot mode</p>
|
||||
<ol class="steps-list">
|
||||
<li><span class="num">1</span> Set reTerminal to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
|
||||
<li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
|
||||
<li><span class="num">2</span> Connect <strong>USB slave</strong> to the host and power on. The device appears above; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li>
|
||||
<li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li>
|
||||
</ol>
|
||||
@@ -485,7 +485,7 @@
|
||||
</details>
|
||||
|
||||
<!-- 5. Recent log (collapsible) -->
|
||||
<details class="section" style="padding:0;">
|
||||
<details class="section" style="padding:0;" open>
|
||||
<summary>Recent log</summary>
|
||||
<div class="inner">
|
||||
<pre id="log" class="log-pre"></pre>
|
||||
@@ -516,7 +516,7 @@
|
||||
const phase = data.phase || 'idle';
|
||||
statusPill.className = 'status-pill ' + phase;
|
||||
statusPill.textContent = phaseLabels[phase] || phase;
|
||||
statusMsg.textContent = data.message || '';
|
||||
statusMsg.textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
|
||||
|
||||
if (data.error) {
|
||||
statusErr.textContent = data.error;
|
||||
@@ -566,7 +566,7 @@
|
||||
if (shrinkWrap) shrinkWrap.style.display = 'block';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'device-item';
|
||||
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup, Deploy, or Update EEPROM</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button><select class="eeprom-preset" title="Boot order"><option value="0x1">eMMC only</option></select><button type="button" class="btn btn-outline" data-source="usb" data-action="eeprom_update">Update EEPROM</button></div>';
|
||||
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||
container.appendChild(el);
|
||||
} else {
|
||||
if (shrinkWrap) shrinkWrap.style.display = 'none';
|
||||
@@ -583,10 +583,6 @@
|
||||
const mac = btn.getAttribute('data-mac');
|
||||
const body = { source: source, action: action };
|
||||
if (mac) body.mac = mac;
|
||||
if (action === 'eeprom_update' && source === 'usb') {
|
||||
const presetEl = btn.closest('.device-item') && btn.closest('.device-item').querySelector('.eeprom-preset');
|
||||
body.boot_order = (presetEl && presetEl.value) ? presetEl.value : '0x1';
|
||||
}
|
||||
const shrinkCb = document.getElementById('shrinkAfterBackup');
|
||||
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
|
||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
@@ -785,7 +781,12 @@
|
||||
fetch('/api/pending-devices').then(function(r) { return r.json(); }).then(function(d) { renderPending(d.usb || null, d.network || []); }).catch(function() { renderPending(null, []); });
|
||||
}
|
||||
function fetchLog() {
|
||||
fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) { document.getElementById('log').textContent = d.log || ''; }).catch(function() {});
|
||||
fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) {
|
||||
var logEl = document.getElementById('log');
|
||||
if (!logEl) return;
|
||||
logEl.textContent = d.log || '';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}).catch(function() {});
|
||||
}
|
||||
function fetchBackups() {
|
||||
fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin login · CM4 Provisioning</title>
|
||||
<title>Admin login · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -28,7 +28,7 @@
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Admin login</h1>
|
||||
<p class="sub">CM4 eMMC provisioning dashboard</p>
|
||||
<p class="sub">GNSS Guard Provisioning admin</p>
|
||||
{% if error %}<p class="err">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="{{ url_for('login') }}">
|
||||
<label for="username">Username</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Portal files · Admin · CM4 Provisioning</title>
|
||||
<title>Portal files · Admin · GNSS Guard Provisioning</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
468
emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md
Normal file
468
emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# Deploying the CM4 eMMC Provisioning Stack to Proxmox
|
||||
|
||||
Complete step-by-step guide for deploying the provisioning service (Proxmox host + LXC container) on a new or existing Proxmox server. Covers host preparation, network bridge configuration, LXC deployment, post-deploy setup, network boot, and first-boot asset sync.
|
||||
|
||||
For reference details (troubleshooting, redeploy, architecture), see [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The provisioning stack consists of two parts that work together:
|
||||
|
||||
| Component | Where it runs | What it does |
|
||||
|-----------|--------------|--------------|
|
||||
| **Host scripts + udev** | Proxmox host | Detects CM4 over USB, runs `rpiboot`, then `dd` to write/read the eMMC |
|
||||
| **LXC container** (`cm4-provisioning`) | Proxmox LXC | Runs the Flask dashboard on port 5000; serves portal files and golden images |
|
||||
|
||||
The host and LXC share `/var/lib/cm4-provisioning/` via a bind-mount, so images and status files are visible from both.
|
||||
|
||||
```
|
||||
Workstation (this repo)
|
||||
│
|
||||
│ deploy-to-proxmox.sh (SSH + rsync)
|
||||
▼
|
||||
Proxmox Host
|
||||
├── udev → cm4-flash-trigger.sh → flash-emmc-on-connect.sh
|
||||
├── /opt/cm4-provisioning/ (scripts, env)
|
||||
├── /var/lib/cm4-provisioning/ (golden.img, backups, status.json) ◄──────┐
|
||||
└── LXC: cm4-provisioning │ bind-mount
|
||||
├── Flask dashboard :5000 │
|
||||
├── /opt/cm4-provisioning/dashboard/ │
|
||||
└── /var/lib/cm4-provisioning/ ◄────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Proxmox Host Prerequisites
|
||||
|
||||
### 1.1 Hardware & OS
|
||||
|
||||
- Proxmox VE 7 or 8 installed on a physical machine.
|
||||
- At least one USB port accessible to the host (not passed through to a VM) for the reTerminal USB slave cable.
|
||||
- At least one active storage (local or local-lvm). Check: `pvesm status`.
|
||||
- Internet access on the host (needed for initial usbboot/PiShrink install and LXC template download).
|
||||
|
||||
### 1.2 SSH key access from your workstation
|
||||
|
||||
The deploy script connects as `root` using key-based auth. Set this up if not already done:
|
||||
|
||||
```bash
|
||||
# On your workstation — copy your public key to the Proxmox host
|
||||
ssh-copy-id root@YOUR_PROXMOX_HOST
|
||||
# Verify
|
||||
ssh root@YOUR_PROXMOX_HOST "echo OK"
|
||||
```
|
||||
|
||||
### 1.3 Proxmox network bridges
|
||||
|
||||
The LXC needs at minimum one bridge for WAN access. For provisioning LAN (DHCP to devices), it needs a second bridge.
|
||||
|
||||
#### WAN bridge (required)
|
||||
|
||||
The default WAN bridge is `vmbr0`, which is created automatically by Proxmox during installation and connects to your primary network. No extra configuration needed.
|
||||
|
||||
#### LAN bridge for provisioning (required for network boot / device DHCP)
|
||||
|
||||
If you want the LXC to serve DHCP, TFTP, and DNS on a dedicated provisioning LAN (so devices can network-boot), create a second Linux bridge on the Proxmox host:
|
||||
|
||||
1. Open **Proxmox Web UI → Node → Network → Create → Linux Bridge**.
|
||||
2. Set:
|
||||
- **Name:** `vmbr1` (or any unused bridge name)
|
||||
- **Bridge ports:** the physical NIC connected to your provisioning LAN switch (e.g. `enp2s0`). Leave blank for an internal-only bridge (useful for testing with no physical switch).
|
||||
- **IPv4/CIDR:** leave blank (the LXC handles the IP on this bridge, not the host).
|
||||
- **Autostart:** checked.
|
||||
3. Click **Create**, then **Apply Configuration**.
|
||||
|
||||
> **Note:** If you connect the reTerminals via a switch that is also connected to `enp2s0`, traffic flows directly. If there is no physical NIC to dedicate, leave bridge ports blank and connect all provisioning devices as VMs/LXCs on the same internal bridge.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Running the Deploy Script
|
||||
|
||||
From your **workstation** (where this repo is cloned), run a single command to deploy everything:
|
||||
|
||||
```bash
|
||||
cd /path/to/reTerminal\ DM4
|
||||
|
||||
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
|
||||
Replace `YOUR_PROXMOX_HOST` with the Proxmox hostname or IP address.
|
||||
|
||||
### 2.1 Deploy script environment variables
|
||||
|
||||
Set these before running the script to customise the deployment:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DEPLOY_ROOTFS_STORAGE` | *(interactive)* | LXC rootfs storage name (e.g. `local-lvm`). If not set, script lists storages and asks. |
|
||||
| `DEPLOY_LXC_WAN_BRIDGE` | `vmbr0` | Proxmox bridge for WAN (eth0 in LXC). |
|
||||
| `DEPLOY_LXC_WAN_IP` | `dhcp` | WAN address: `dhcp` or a static IP like `192.168.1.10/24`. |
|
||||
| `DEPLOY_LXC_LAN_BRIDGE` | *(none)* | If set, adds eth1 as provisioning LAN on this bridge (e.g. `vmbr1`). |
|
||||
| `DEPLOY_LXC_LAN_SUBNET` | `10.20.50.1/24` | LXC IP/prefix on the LAN bridge. Used only when `DEPLOY_LXC_LAN_BRIDGE` is set. |
|
||||
| `DEPLOY_LXC_ROOT_PASSWORD` | *(default)* | Sets LXC root password and enables SSH inside the container. |
|
||||
| `DEPLOY_LXC_SSH_KEY` | `~/.ssh/id_ed25519.pub` | Public key to add to LXC root's `authorized_keys`. Defaults to your workstation key. |
|
||||
| `CM4_BACKUPS_HOST_PATH` | *(none)* | Host directory for backup images (e.g. `/mnt/storage/cm4-backups`). Bind-mounted into LXC. |
|
||||
| `DEPLOY_EMMC_SIZE_GB` | `32` | eMMC size hint in GB (used only when multiple block devices appear after rpiboot). |
|
||||
| `DEPLOY_LOG` | *(off)* | Set to `1` to write a timestamped log file in `scripts/`. |
|
||||
|
||||
### 2.2 Full deploy example
|
||||
|
||||
```bash
|
||||
DEPLOY_ROOTFS_STORAGE=local-lvm \
|
||||
DEPLOY_LXC_ROOT_PASSWORD='YourSecurePassword' \
|
||||
DEPLOY_LXC_LAN_BRIDGE=vmbr1 \
|
||||
DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 \
|
||||
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.20.30.40
|
||||
```
|
||||
|
||||
### 2.3 What the deploy script does
|
||||
|
||||
The script runs five stages:
|
||||
|
||||
1. **Check** — SSHes to the host, finds existing `cm4-provisioning` container by hostname (or lists storage for new container creation).
|
||||
2. **Clean + Rsync** — Wipes `/tmp/emmc-provisioning-deploy` on the host and rsyncs the entire repo there (excluding `.git` and deploy logs).
|
||||
3. **Remote install (host + LXC)** — Runs a remote heredoc that:
|
||||
- Creates the LXC (Debian 12, 1 GB RAM, 8 GB rootfs) if it doesn't exist, or reuses it by hostname.
|
||||
- Adds eth1 (LAN bridge) if `DEPLOY_LXC_LAN_BRIDGE` is set.
|
||||
- Configures the bind-mount for `/var/lib/cm4-provisioning/`.
|
||||
- Installs host scripts to `/opt/cm4-provisioning/` and udev rules to `/etc/udev/rules.d/`.
|
||||
- Installs and enables systemd units: `cm4-flash.service`, `cm4-build-cloudinit.path/.service`, `cm4-shrink.path/.service`.
|
||||
- Writes `/opt/cm4-provisioning/env` (golden image path, rpiboot dir, eMMC size).
|
||||
- Installs `python3-flask` and `openssh-server` in the LXC (skipped if already present).
|
||||
- Deploys the Flask dashboard and enables/restarts `cm4-dashboard.service` in the LXC.
|
||||
- Installs usbboot (`rpiboot`) on the host if not already present.
|
||||
- Installs PiShrink on the host if not already present.
|
||||
4. **LXC start** — Starts the LXC if stopped.
|
||||
5. **Summary** — Prints LXC WAN IP (and LAN IP if set), dashboard URL, and remaining manual steps.
|
||||
|
||||
On **redeploy** (container already exists): host scripts, dashboard, env, systemd, and udev are always updated. LXC creation, bind-mounts, apt installs, usbboot, and PiShrink are skipped when already present.
|
||||
|
||||
---
|
||||
|
||||
## Part 3 — Post-Deploy: Required Manual Steps
|
||||
|
||||
### Step 1: Verify the dashboard is up
|
||||
|
||||
```bash
|
||||
# Get the LXC IP from deploy output, or:
|
||||
ssh root@YOUR_PROXMOX_HOST \
|
||||
"CID=\$(pct list | awk '\$3==\"cm4-provisioning\"{print \$1}'); pct exec \$CID -- hostname -I"
|
||||
|
||||
# Open the dashboard
|
||||
open http://<LXC-IP>:5000
|
||||
```
|
||||
|
||||
The dashboard should show **"Waiting for device in USB boot mode"** on the home page.
|
||||
|
||||
### Step 2: Verify host services
|
||||
|
||||
SSH to the Proxmox host and confirm the host side is healthy:
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST
|
||||
|
||||
# Check udev rule is installed
|
||||
ls /etc/udev/rules.d/90-cm4-boot-mode.rules
|
||||
|
||||
# Check flash trigger script
|
||||
ls /usr/local/bin/cm4-flash-trigger.sh
|
||||
|
||||
# Check host scripts
|
||||
ls /opt/cm4-provisioning/
|
||||
# Expected: flash-emmc-on-connect.sh, build-cloudinit-image.sh,
|
||||
# run-shrink-on-host.sh, fix-gadget-bootcode-on-host.sh, env
|
||||
|
||||
# Check systemd path units are active
|
||||
systemctl status cm4-build-cloudinit.path cm4-shrink.path
|
||||
|
||||
# Check auto-flash is enabled
|
||||
ls /etc/cm4-provisioning/enabled
|
||||
```
|
||||
|
||||
### Step 3: Add a golden image
|
||||
|
||||
A **golden image** is required for **Deploy** (writing an image to the device's eMMC). Backup (reading from device) works without it.
|
||||
|
||||
**Option A — Build via the dashboard:**
|
||||
1. Open `http://<LXC-IP>:5000` → Admin tab.
|
||||
2. Click **Build cloud-init image**: the host downloads the latest Raspberry Pi OS, injects your cloud-init `user-data`, and creates `golden.img`.
|
||||
3. Click **Set as golden** once the build finishes.
|
||||
|
||||
**Option B — Copy an existing image:**
|
||||
```bash
|
||||
scp /path/to/your-golden.img root@YOUR_PROXMOX_HOST:/var/lib/cm4-provisioning/golden.img
|
||||
```
|
||||
|
||||
**Option C — Promote a backup:**
|
||||
In the dashboard Admin → Images tab, select a backup and click **Set as golden**.
|
||||
|
||||
### Step 4: Enable SSH into the LXC (optional)
|
||||
|
||||
If you ran the deploy with `DEPLOY_LXC_ROOT_PASSWORD` or a default SSH key, the LXC already has SSH enabled. Otherwise:
|
||||
|
||||
```bash
|
||||
# From your workstation — adds your default SSH key and enables root SSH
|
||||
./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST
|
||||
|
||||
# Or with a specific key and password
|
||||
ROOT_PASSWORD='YourPassword' \
|
||||
./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
Then connect:
|
||||
```bash
|
||||
ssh root@<LXC-IP>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4 — Sync Portal Files (First-Boot Assets)
|
||||
|
||||
The LXC serves first-boot assets (kiosk scripts, desktop files, splash, theme, etc.) from `/var/lib/cm4-provisioning/portal-files/`. These must be synced from the repo.
|
||||
|
||||
```bash
|
||||
# From your workstation
|
||||
./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
This rsyncs everything under `emmc-provisioning/cloud-init/fileserver/` to the LXC portal-files directory. Run this every time you update kiosk assets or first-boot scripts.
|
||||
|
||||
**What gets synced:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `start-chromium.sh` | Wayland/labwc Chromium kiosk launcher |
|
||||
| `five-tap-close-chromium.py` | 5-tap touch overlay to close Chromium |
|
||||
| `chromium-kiosk.desktop` | Autostart: launches Chromium kiosk |
|
||||
| `chromium-kiosk-no-select/` | Chromium extension: disables text selection |
|
||||
| `set-rotation-at-login.sh/.desktop` | Per-login screen rotation |
|
||||
| `01-set-rotation-once.sh/.desktop` | One-shot: rotation + dark theme + kanshi |
|
||||
| `02-set-wallpaper-once.sh/.desktop` | One-shot: set wallpaper |
|
||||
| `99-default-session.conf` | LightDM session = `rpd-labwc` |
|
||||
| `custom.plymouth` + `custom.script` | Plymouth boot splash theme |
|
||||
| `splash.png` | Boot splash / wallpaper image |
|
||||
| `steps/01–13*.sh` | First-boot step scripts sourced by `first-boot.sh` |
|
||||
|
||||
---
|
||||
|
||||
## Part 5 — Network Boot Setup (Optional)
|
||||
|
||||
Only needed if you want devices to **boot over the network** (PXE-style via TFTP) for provisioning, rather than via USB cable.
|
||||
|
||||
### 5.1 Prerequisites
|
||||
|
||||
- The LXC must have been deployed with a **LAN bridge** (`DEPLOY_LXC_LAN_BRIDGE` set). The LXC's eth1 will be the provisioning LAN gateway.
|
||||
- Devices must be connected to the same LAN as the LXC's eth1.
|
||||
|
||||
### 5.2 Run the network boot setup script
|
||||
|
||||
From your workstation:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
This SSH-connects to the LXC and runs the full setup inside the container. It performs:
|
||||
|
||||
1. **Installs dnsmasq** (DHCP + DNS server) and the `vlan` package (for VLAN interfaces).
|
||||
2. **Configures dnsmasq** on eth1:
|
||||
- DHCP range: `<LAN_BASE>.100` – `<LAN_BASE>.200` (e.g. `10.20.50.100`–`10.20.50.200`).
|
||||
- DNS: static record `file.server` → LAN gateway IP, so first-boot scripts can reach `http://file.server/...`.
|
||||
- DHCP option 6: sends LXC as DNS server to all DHCP clients.
|
||||
3. **Configures extra IPs on eth1**: `192.168.30.1/24`, `192.168.127.1/24` (for serving multiple subnets).
|
||||
4. **Creates VLAN 40** interface `eth1.40` at `192.168.0.1/24` (for VLAN-tagged networks on the provisioning LAN).
|
||||
5. **Enables IP forwarding** (`net.ipv4.ip_forward=1`) persisted in `/etc/sysctl.d/`.
|
||||
6. **Configures NAT** (nftables or iptables fallback): masquerades all LAN traffic out eth0 so devices on the provisioning LAN get internet access.
|
||||
7. **Enables and starts dnsmasq**.
|
||||
|
||||
Config files written:
|
||||
- `/etc/dnsmasq.d/network-boot.conf` — DHCP + DNS on eth1
|
||||
- `/etc/nftables.d/nat-lan.conf` — NAT rules
|
||||
- `/etc/network/interfaces.d/70-cm4-extra-lan` — extra IPs and VLAN persisted
|
||||
|
||||
### 5.3 Enable PXE / TFTP network boot
|
||||
|
||||
TFTP is not enabled by default (dnsmasq is configured for DHCP + DNS only). To enable PXE/TFTP so devices can load a kernel and initramfs over the network:
|
||||
|
||||
```bash
|
||||
# SSH into the LXC
|
||||
ssh root@<LXC-IP>
|
||||
|
||||
# Enable PXE/TFTP (adds the PXE options to dnsmasq and restarts it)
|
||||
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable
|
||||
```
|
||||
|
||||
This activates the PXE snippet at `/etc/dnsmasq.d/network-boot-pxe.conf` (DHCP options 66/67: next-server + boot file) and reloads dnsmasq.
|
||||
|
||||
To disable PXE again (keep DHCP/DNS only):
|
||||
```bash
|
||||
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh disable
|
||||
```
|
||||
|
||||
### 5.4 Populate the TFTP boot files
|
||||
|
||||
The TFTP root (`/srv/tftpboot`) needs Raspberry Pi 4 / CM4 boot files. From your workstation:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/populate-tftpboot-from-git.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
This downloads the official Raspberry Pi firmware `boot/` folder from GitHub into `/srv/tftpboot` on the LXC.
|
||||
|
||||
To add the custom provisioning initramfs (Alpine-based, allows Backup/Deploy from network boot):
|
||||
|
||||
```bash
|
||||
# Ensure the initramfs image is built (or use the pre-built one in the repo)
|
||||
ls emmc-provisioning/network-boot-initramfs/initrd.img
|
||||
|
||||
# Copy it to the LXC TFTP root
|
||||
scp emmc-provisioning/network-boot-initramfs/initrd.img root@<LXC-IP>:/srv/tftpboot/
|
||||
|
||||
# Then ensure config.txt references it
|
||||
./emmc-provisioning/scripts/ensure-tftpboot-config-kernel-initrd.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
### 5.5 Configure device EEPROM for network boot
|
||||
|
||||
For a reTerminal to boot from the network (when eMMC is empty or network boot order is set), its EEPROM `BOOT_ORDER` must include network boot. The recommended order is `0xf21` (eMMC first, then network):
|
||||
|
||||
```bash
|
||||
# Check current EEPROM boot order on a connected device
|
||||
./emmc-provisioning/scripts/check-network-boot-priority.sh root@<DEVICE-IP>
|
||||
```
|
||||
|
||||
To set boot order via the provisioning dashboard (when device is in USB boot mode), use the **Update EEPROM** button in the dashboard.
|
||||
|
||||
### 5.6 Verify network boot is working
|
||||
|
||||
On the LXC, check the following:
|
||||
|
||||
```bash
|
||||
# Is dnsmasq running?
|
||||
systemctl status dnsmasq
|
||||
|
||||
# Is TFTP/PXE enabled?
|
||||
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh status
|
||||
|
||||
# Are TFTP boot files present?
|
||||
ls /srv/tftpboot/start4cd.elf
|
||||
|
||||
# Any DHCP leases from devices?
|
||||
cat /var/lib/misc/dnsmasq.leases
|
||||
|
||||
# Monitor live DHCP/TFTP traffic when powering on a device
|
||||
tcpdump -i eth1 -n port 67 or port 68 or port 69
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 6 — Installing usbboot Manually (if needed)
|
||||
|
||||
If the deploy script could not install usbboot (e.g. no internet during deploy), install it manually:
|
||||
|
||||
```bash
|
||||
# From your workstation
|
||||
scp emmc-provisioning/scripts/install-usbboot-on-host.sh root@YOUR_PROXMOX_HOST:/tmp/
|
||||
ssh root@YOUR_PROXMOX_HOST "bash /tmp/install-usbboot-on-host.sh"
|
||||
```
|
||||
|
||||
Or, if `/tmp/emmc-provisioning-deploy` is still on the host:
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh"
|
||||
```
|
||||
|
||||
After install, verify:
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST "ls /opt/usbboot/rpiboot && ls /opt/usbboot/mass-storage-gadget64/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 7 — Installing PiShrink Manually (if needed)
|
||||
|
||||
PiShrink enables the dashboard **Shrink/Compress** function (shrinks backup images before compressing). Install if the deploy failed:
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh"
|
||||
# Or stream from workstation:
|
||||
ssh root@YOUR_PROXMOX_HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 8 — Updating / Redeploying
|
||||
|
||||
To push code changes (scripts, dashboard, udev, systemd) to an existing deployment:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
|
||||
The script finds the container by hostname (`cm4-provisioning`) and updates all files. It does **not** overwrite your `golden.img` or `/etc/cm4-provisioning/enabled`.
|
||||
|
||||
To update **only the dashboard** (faster when only `app.py` or templates changed):
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/deploy-dashboard-to-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
To update **only the portal files** (kiosk assets, first-boot scripts):
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/sync-portal-files-to-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
| # | Action | Script / Command | Required? |
|
||||
|---|--------|-----------------|-----------|
|
||||
| **Host prep** | | | |
|
||||
| 1 | SSH key access to Proxmox host | `ssh-copy-id root@HOST` | **Yes** |
|
||||
| 2 | Create LAN bridge on Proxmox (`vmbr1`) | Proxmox Web UI | For network boot |
|
||||
| **Deploy** | | | |
|
||||
| 3 | Run deploy script | `deploy-to-proxmox.sh root@HOST` | **Yes** |
|
||||
| 4 | Verify dashboard is up | `http://<LXC-IP>:5000` | **Yes** |
|
||||
| 5 | Verify host services and udev rule | `ssh root@HOST "ls /etc/udev/rules.d/90-cm4-boot-mode.rules"` | **Yes** |
|
||||
| **Post-deploy** | | | |
|
||||
| 6 | Add golden image for Deploy | Dashboard Admin or `scp golden.img root@HOST:/var/lib/cm4-provisioning/` | For Deploy |
|
||||
| 7 | Sync portal files (kiosk/first-boot assets) | `sync-portal-files-to-lxc.sh root@<LXC-IP>` | For first-boot provisioning |
|
||||
| 8 | Enable SSH into LXC | `setup-lxc-ssh.sh root@HOST` | Optional |
|
||||
| **Network boot** | | | |
|
||||
| 9 | Run network boot setup on LXC | `setup-network-boot-on-lxc.sh root@<LXC-IP>` | For network boot only |
|
||||
| 10 | Enable PXE/TFTP | `ssh root@<LXC-IP> /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable` | For PXE boot |
|
||||
| 11 | Populate TFTP boot files | `populate-tftpboot-from-git.sh root@<LXC-IP>` | For PXE boot |
|
||||
| 12 | Copy provisioning initramfs to TFTP | `scp network-boot-initramfs/initrd.img root@<LXC-IP>:/srv/tftpboot/` | For provisioning via netboot |
|
||||
| 13 | Configure device EEPROM boot order | Dashboard **Update EEPROM** or `rpi-eeprom-config` | For network boot |
|
||||
| **If needed** | | | |
|
||||
| 14 | Install usbboot manually | `install-usbboot-on-host.sh` | If deploy had no internet |
|
||||
| 15 | Install PiShrink manually | `install-pishrink-on-host.sh` | If deploy had no internet |
|
||||
|
||||
---
|
||||
|
||||
## After Deployment: Quick Reference
|
||||
|
||||
| What | How |
|
||||
|------|-----|
|
||||
| **Dashboard (WAN)** | `http://<LXC-IP>:5000` |
|
||||
| **Dashboard (LAN)** | `http://10.20.50.1:5000` (if LAN bridge was set) |
|
||||
| **SSH to LXC** | `ssh root@<LXC-IP>` |
|
||||
| **Get LXC IP** | `ssh root@HOST "pct list; pct exec <CTID> -- hostname -I"` |
|
||||
| **Golden image path** | `/var/lib/cm4-provisioning/golden.img` (same on host and in LXC) |
|
||||
| **Disable auto-flash** | `ssh root@HOST "rm /etc/cm4-provisioning/enabled"` |
|
||||
| **Re-enable auto-flash** | `ssh root@HOST "touch /etc/cm4-provisioning/enabled"` |
|
||||
| **Flash log (on host)** | `ssh root@HOST "tail -f /var/lib/cm4-provisioning/flash.log"` |
|
||||
| **Status JSON (on host)** | `ssh root@HOST "cat /var/lib/cm4-provisioning/status.json"` |
|
||||
| **Full host snapshot** | `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/monitor-from-host.sh` |
|
||||
| **DHCP leases (LXC)** | `ssh root@<LXC-IP> "cat /var/lib/misc/dnsmasq.leases"` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For USB flash errors (rpiboot failures, block device not found, USB transfer errors) and LXC/dashboard issues, see the full troubleshooting section in [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md).
|
||||
|
||||
For network boot issues (DHCP not working, device not appearing in dashboard), see [NETWORK-BOOT-TROUBLESHOOTING.md](NETWORK-BOOT-TROUBLESHOOTING.md).
|
||||
90
emmc-provisioning/docs/DEVICE-DNS-DHCP-RESOLVCONF.md
Normal file
90
emmc-provisioning/docs/DEVICE-DNS-DHCP-RESOLVCONF.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Device DNS from DHCP and /etc/resolv.conf
|
||||
|
||||
This document describes how to configure provisioned devices (e.g. Raspberry Pi / reTerminal) so they **use DNS from DHCP** and do **not** have a fixed nameserver in `/etc/resolv.conf`. That way the LXC’s dnsmasq (option 6) is used, **file.server** resolves, and scripts can use `http://file.server/...` without hardcoding IPs.
|
||||
|
||||
## Summary of changes (what we did on the Pi)
|
||||
|
||||
1. **Do not overwrite `/etc/resolv.conf`**
|
||||
No script (e.g. bootstrap or first-boot) should write a fixed nameserver (e.g. `8.8.8.8`) into `/etc/resolv.conf`. DNS should come from DHCP.
|
||||
|
||||
2. **LXC sends DHCP option 6 (DNS server)**
|
||||
dnsmasq on the LXC must send the LXC’s eth1 IP as the DNS server so clients use it and get **file.server** resolution. See [DNSMASQ-DNS-FILESERVER.md](DNSMASQ-DNS-FILESERVER.md) and `scripts/setup-network-boot-on-lxc.sh` (`dhcp-option=6,${LAN_GW}`).
|
||||
|
||||
3. **Let NetworkManager manage `/etc/resolv.conf`**
|
||||
So that the nameserver in `/etc/resolv.conf` is the one from DHCP (option 6), either:
|
||||
- **Option A (recommended for full cloud-init):** Use **systemd-resolved** and make `/etc/resolv.conf` a symlink to the resolved stub; resolved gets DNS from NetworkManager.
|
||||
- **Option B (minimal cloud-init):** Use **NetworkManager** to manage `/etc/resolv.conf` via a symlink: add `rc-manager=symlink` in NetworkManager config so `/etc/resolv.conf` points to `/run/NetworkManager/resolv.conf`, which NM fills with the DHCP DNS.
|
||||
|
||||
4. **Ensure connection uses DHCP DNS**
|
||||
The NetworkManager connection should have `ipv4.ignore-auto-dns: no` (default) so it accepts option 6 from DHCP. No fixed `ipv4.dns` in the connection.
|
||||
|
||||
## What to change in cloud-init
|
||||
|
||||
### Option A: user-data.bootstrap (uses Option B for RPi OS)
|
||||
|
||||
**File:** `cloud-init/user-data.bootstrap`
|
||||
|
||||
- **manage_resolv_conf: false** — already set; cloud-init must not overwrite resolv.conf.
|
||||
- **NetworkManager** — `99-resolv-dhcp.conf` has `rc-manager=symlink` so NM creates `/etc/resolv.conf` with DHCP DNS. RPi OS does not use systemd-resolved by default.
|
||||
- **runcmd** — removes static resolv.conf and restarts NM so it creates the symlink with DHCP option 6.
|
||||
- **Bootstrap script** — must **not** write `nameserver 8.8.8.8` (or any fixed server) into `/etc/resolv.conf`. Our `bootstrap.sh` no longer does that.
|
||||
|
||||
No extra changes needed if you use `user-data.bootstrap` as-is; just ensure your bootstrap script does not touch resolv.conf.
|
||||
|
||||
### Option B: Minimal user-data (first-boot or bootstrap-only, no systemd-resolved)
|
||||
|
||||
If your user-data only runs a remote script (e.g. `first-boot.sh` or `bootstrap.sh`) and does **not** enable systemd-resolved, add the following so the device uses DNS from DHCP and NM manages resolv.conf:
|
||||
|
||||
1. **Set in user-data (cloud-config):**
|
||||
```yaml
|
||||
manage_resolv_conf: false
|
||||
```
|
||||
|
||||
2. **Add a write_files entry** so NetworkManager manages resolv.conf with the DHCP-provided DNS:
|
||||
```yaml
|
||||
write_files:
|
||||
# ... your other write_files ...
|
||||
- path: /etc/NetworkManager/conf.d/99-resolv-dhcp.conf
|
||||
content: |
|
||||
[main]
|
||||
rc-manager=symlink
|
||||
permissions: '0644'
|
||||
```
|
||||
|
||||
3. **In your bootstrap/first-boot script:**
|
||||
Do **not** write a fixed nameserver to `/etc/resolv.conf` (e.g. remove any line like `echo "nameserver 8.8.8.8" > /etc/resolv.conf`).
|
||||
|
||||
4. **Optional runcmd** (if you want a clean state on first boot):
|
||||
Remove any existing static resolv.conf so NM can create its symlink and write DHCP DNS:
|
||||
```yaml
|
||||
runcmd:
|
||||
- rm -f /etc/resolv.conf
|
||||
- systemctl restart NetworkManager
|
||||
# ... then your download and run of bootstrap.sh or first-boot.sh ...
|
||||
```
|
||||
|
||||
After first boot, devices will get DNS from DHCP (LXC option 6), and **file.server** will resolve to the LXC’s eth1 IP.
|
||||
|
||||
## Verification on the device
|
||||
|
||||
```bash
|
||||
# Should show the LXC as nameserver (e.g. 10.20.40.1), not 8.8.8.8
|
||||
cat /etc/resolv.conf
|
||||
|
||||
# Should resolve to LXC eth1
|
||||
getent hosts file.server
|
||||
```
|
||||
|
||||
## Reference: manual fix on an already-provisioned device
|
||||
|
||||
If a device was provisioned before these changes and still has a fixed DNS (e.g. 8.8.8.8):
|
||||
|
||||
1. **LXC:** Ensure dnsmasq sends option 6 (see [DNSMASQ-DNS-FILESERVER.md](DNSMASQ-DNS-FILESERVER.md)); re-run `setup-network-boot-on-lxc.sh` if needed.
|
||||
2. **On the device:**
|
||||
- Add NetworkManager config:
|
||||
`echo -e '[main]\nrc-manager=symlink' | sudo tee /etc/NetworkManager/conf.d/99-resolv-dhcp.conf`
|
||||
- Remove static resolv.conf and restart NM:
|
||||
`sudo rm -f /etc/resolv.conf && sudo systemctl restart NetworkManager`
|
||||
- Renew DHCP so the device gets option 6:
|
||||
`sudo nmcli con down "Wired connection 1"; sudo nmcli con up "Wired connection 1"`
|
||||
3. Check: `cat /etc/resolv.conf` and `getent hosts file.server`.
|
||||
101
emmc-provisioning/docs/DNSMASQ-DNS-FILESERVER.md
Normal file
101
emmc-provisioning/docs/DNSMASQ-DNS-FILESERVER.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# dnsmasq DNS and file.server on the LXC
|
||||
|
||||
This document describes the dnsmasq DNS configuration on the provisioning LXC and the static hostname **file.server** used for the fileserver.
|
||||
|
||||
## What was changed
|
||||
|
||||
### 1. dnsmasq now provides DNS on eth1
|
||||
|
||||
Previously, dnsmasq on the LXC was configured with **`port=0`**, which disabled DNS and provided only DHCP and TFTP on the provisioning interface (eth1).
|
||||
|
||||
**Change:** `port=0` was removed so dnsmasq also acts as a DNS server on eth1 (port 53). Clients that receive DHCP from dnsmasq will use the LXC as their DNS server for the provisioning LAN.
|
||||
|
||||
- **DHCP** on eth1 — unchanged (range from `lan-subnet.conf`, e.g. 10.20.40.100–10.20.40.200).
|
||||
- **TFTP/PXE** on eth1 — unchanged (toggle with `/opt/cm4-provisioning/toggle-network-boot-dhcp.sh`).
|
||||
- **DNS** on eth1 — **new**: local static records (e.g. `file.server`) plus forwarding of other queries via the LXC’s `/etc/resolv.conf`.
|
||||
|
||||
### 2. Static DNS record: file.server → eth1 IP
|
||||
|
||||
A static A record was added so the hostname **file.server** resolves to the LXC’s eth1 address (the provisioning LAN gateway). That IP is taken from **`/opt/cm4-provisioning/lan-subnet.conf`** as **`LAN_GW`** (e.g. `10.20.40.1`).
|
||||
|
||||
**dnsmasq config (written by `setup-network-boot-on-lxc.sh`):**
|
||||
|
||||
```text
|
||||
address=/file.server/${LAN_GW}
|
||||
```
|
||||
|
||||
So scripts and devices on the provisioning LAN can use **`http://file.server/...`** (or `file.server` in general) without hardcoding the LXC’s IP. The IP stays correct even if the LAN subnet is changed and the setup script is re-run.
|
||||
|
||||
### 3. Files modified in the repo
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| **scripts/setup-network-boot-on-lxc.sh** | Removed `port=0`; added `address=/file.server/${LAN_GW}` and comments in the generated `/etc/dnsmasq.d/network-boot.conf`. |
|
||||
| **lxc/dnsmasq-network-boot.conf** | Template updated: removed `port=0`, added comment for DNS and `file.server` (commented example). |
|
||||
|
||||
### 4. Applied on LXC (root@10.20.40.1)
|
||||
|
||||
On **2025-03-04** the setup script was run against **root@10.20.40.1**:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@10.20.40.1
|
||||
```
|
||||
|
||||
Result on that LXC:
|
||||
|
||||
- **LAN:** 10.20.40.0/24, gateway 10.20.40.1 (from existing `lan-subnet.conf`).
|
||||
- **DHCP:** 10.20.40.100–10.20.40.200 on eth1.
|
||||
- **DNS:** Enabled on eth1; **file.server** → **10.20.40.1**.
|
||||
- dnsmasq and NAT were (re)configured; TFTP root and network boot toggle unchanged.
|
||||
|
||||
So on the provisioning LAN, **file.server** resolves to **10.20.40.1** (the LXC’s eth1).
|
||||
|
||||
## How to use file.server in scripts
|
||||
|
||||
On devices that get DHCP (and thus DNS) from the LXC on the provisioning LAN:
|
||||
|
||||
- Use **`http://file.server/...`** (or `file.server` as hostname) instead of `http://10.20.40.1/...`.
|
||||
- No need to hardcode the LXC IP; if you change the subnet and re-run the setup script, **file.server** will still point at the correct gateway.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl -O http://file.server/cloud-init/seed.img
|
||||
```
|
||||
|
||||
## Adding more static DNS entries
|
||||
|
||||
To add more names (e.g. `fileserver` or another hostname), add more **`address=/name/${LAN_GW}`** lines in the heredoc in **scripts/setup-network-boot-on-lxc.sh** (where `network-boot.conf` is generated), or add a separate file under `/etc/dnsmasq.d/` on the LXC with the same format. Then restart dnsmasq:
|
||||
|
||||
```bash
|
||||
systemctl restart dnsmasq
|
||||
```
|
||||
|
||||
## Re-applying on another or existing LXC
|
||||
|
||||
To apply or refresh this configuration on any LXC:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP> [SUBNET]
|
||||
```
|
||||
|
||||
Example with explicit subnet:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@10.20.40.1 10.20.40.1/24
|
||||
```
|
||||
|
||||
This rewrites `/etc/dnsmasq.d/network-boot.conf` (including `address=/file.server/${LAN_GW}`) and restarts dnsmasq.
|
||||
|
||||
## Verification on the LXC
|
||||
|
||||
```bash
|
||||
# DNS and file.server
|
||||
grep -E 'address=|port=' /etc/dnsmasq.d/network-boot.conf
|
||||
|
||||
# Resolve file.server (from a client on the provisioning LAN, or from LXC with server 127.0.0.1)
|
||||
getent hosts file.server
|
||||
# or: dig @10.20.40.1 file.server
|
||||
```
|
||||
|
||||
Expected: **file.server** resolves to the LAN gateway (e.g. 10.20.40.1).
|
||||
@@ -1,4 +1,4 @@
|
||||
# How to edit cloud-init files on the device before capturing the image
|
||||
# How to edit cloud-init files on the device or on an image file
|
||||
|
||||
The cloud-init **NoCloud** files live on the **boot partition**. On the running device they are at:
|
||||
|
||||
@@ -77,6 +77,34 @@ They are owned by **root** and need **sudo** to edit.
|
||||
|
||||
---
|
||||
|
||||
## Method 3: Edit on an image file (.img or .img.xz)
|
||||
|
||||
To edit cloud-init **inside a disk image** (e.g. `gnss-bootstrap-20260223-215010.img.xz`) without booting the device:
|
||||
|
||||
1. **Run the helper script** (requires `sudo` for loop device and mount):
|
||||
|
||||
```bash
|
||||
cd /path/to/reTerminal-DM4
|
||||
./emmc-provisioning/scripts/edit-cloudinit-on-image.sh gnss-bootstrap-20260223-215010.img.xz
|
||||
```
|
||||
|
||||
2. The script will:
|
||||
- Decompress the `.img.xz` (if needed; allow ~3GB free space and a minute or two)
|
||||
- Attach the image with `losetup` and mount the **boot** (FAT32) partition
|
||||
- Open your `$EDITOR` (or `nano`) on `user-data`, `meta-data`, and `network-config`
|
||||
- After you save and exit, unmount and recompress, **overwriting** the original `.img.xz`
|
||||
|
||||
3. **Back up the image** before running if you want to keep the original:
|
||||
```bash
|
||||
cp gnss-bootstrap-20260223-215010.img.xz gnss-bootstrap-20260223-215010.img.xz.bak
|
||||
```
|
||||
|
||||
4. **Options:**
|
||||
- `--no-recompress` — Leave the image decompressed after editing (do not overwrite the `.img.xz`).
|
||||
- `--replace-with-repo` — Copy `user-data`, `meta-data`, and `network-config` from `emmc-provisioning/cloud-init/` into the image before opening the editor (useful to start from repo templates).
|
||||
|
||||
---
|
||||
|
||||
## What to edit (typical)
|
||||
|
||||
- **meta-data**
|
||||
|
||||
280
emmc-provisioning/docs/LITE-WAYLAND-KIOSK-DEPLOYMENT.md
Normal file
280
emmc-provisioning/docs/LITE-WAYLAND-KIOSK-DEPLOYMENT.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Deploying Lite + Wayland Kiosk on reTerminal DM
|
||||
|
||||
This guide describes how to deploy a **minimal kiosk** on the reTerminal DM (Raspberry Pi CM4-based) using **Raspberry Pi OS Lite** plus a **Wayland compositor** (no full PIXEL desktop). The result is a device that boots to **Chromium in kiosk mode** with your **brightness overlay** on top; if Chromium exits, you can relaunch it so the user has no other UI to access.
|
||||
|
||||
**See also:**
|
||||
|
||||
- [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) — brightness daemon, API, and overlay installation.
|
||||
- [SCREEN-BRIGHTNESS-SUMMARY.md](SCREEN-BRIGHTNESS-SUMMARY.md) — overview of the brightness system.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **Raspberry Pi OS Lite** | Base OS; no PIXEL, no full desktop. |
|
||||
| **Wayland compositor (Weston)** | Minimal display server; runs Chromium and the brightness overlay. |
|
||||
| **Chromium** | Kiosk browser (fullscreen, single URL). |
|
||||
| **screen-brightness + brightness-api** | System services; backlight and HTTP API (unchanged). |
|
||||
| **brightness-overlay** | GTK + GtkLayerShell; stays on top of Chromium on Wayland. |
|
||||
|
||||
Benefits of Lite + Wayland (vs Lite + X11):
|
||||
|
||||
- **Brightness overlay** uses **GtkLayerShell** natively, so it stays above fullscreen Chromium without window-manager hacks.
|
||||
- No full desktop; fewer packages and a smaller attack surface.
|
||||
- Wayland’s input and security model.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
- **Hardware:** reTerminal DM (CM4) with display, and (for brightness) backlight, buzzer, and light sensor (see [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md#2-verify-hardware)).
|
||||
- **Base image:** Raspberry Pi OS **Lite** (64-bit recommended). Either:
|
||||
- Flash **Raspberry Pi OS Lite** from [raspberrypi.com/software](https://www.raspberrypi.com/software/), or
|
||||
- Start from an existing full OS and remove the desktop (more work; starting from Lite is simpler).
|
||||
- **Network:** Device can reach the internet (or your mirror) for `apt`.
|
||||
- **reTerminal DM drivers:** Installed so `/sys/class/backlight/lcd_backlight` and IIO light sensor exist (Seeed [reTerminal DM wiki](https://wiki.seeedstudio.com/reterminal-dm/)).
|
||||
|
||||
---
|
||||
|
||||
## 3. Install base system (Lite)
|
||||
|
||||
If you are not already on Lite:
|
||||
|
||||
1. Download **Raspberry Pi OS Lite** (e.g. 64-bit) and write it to the eMMC/SD as usual.
|
||||
2. Boot, expand filesystem if needed, set hostname/locale, and ensure the reTerminal DM kernel/drivers are installed (e.g. from Seeed’s repo or your image).
|
||||
3. (Optional) Configure cloud-init or your first-boot flow so new images get the same base.
|
||||
|
||||
---
|
||||
|
||||
## 4. Install Wayland and Weston
|
||||
|
||||
On the device (or in your image build):
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
weston \
|
||||
chromium-browser \
|
||||
xwayland
|
||||
```
|
||||
|
||||
- **weston** — Reference Wayland compositor; suitable for kiosks and supports layer-shell (used by the brightness overlay).
|
||||
- **chromium-browser** — Use the Raspberry Pi OS / Debian package so it runs natively on Wayland when `WAYLAND_DISPLAY` is set.
|
||||
- **xwayland** — Optional; only needed if you have X11-only apps.
|
||||
|
||||
Verify Weston runs (for a quick test, start a temporary session):
|
||||
|
||||
```bash
|
||||
weston --tty=1
|
||||
# You should see a Weston desktop. Exit with Ctrl+Alt+Backspace or by killing weston.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Install brightness stack (daemon + API + overlay)
|
||||
|
||||
Install the screen-brightness daemon and the brightness API as **system services** (they do not require a desktop). Then install the overlay so it can be started from the Weston session.
|
||||
|
||||
### 5.1 Daemon and API (systemd)
|
||||
|
||||
Use the same procedure as [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md). From the repo or your file server:
|
||||
|
||||
```bash
|
||||
# Option A: deployment script (from device, with files present or via URL)
|
||||
sudo ./deploy-screen-brightness-to-device.sh
|
||||
# Or: sudo ./deploy-screen-brightness-to-device.sh "http://your-fileserver:5000/files"
|
||||
|
||||
# Option B: manual install (see SCREEN-BRIGHTNESS-MANUAL-SETUP.md)
|
||||
sudo install -m 755 screen-brightness.py /usr/local/bin/
|
||||
sudo install -m 644 screen-brightness.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload && sudo systemctl enable --now screen-brightness.service
|
||||
```
|
||||
|
||||
Then install the **brightness API**:
|
||||
|
||||
```bash
|
||||
sudo install -m 755 brightness-api.py /usr/local/bin/
|
||||
sudo install -m 644 brightness-api.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload && sudo systemctl enable --now brightness-api.service
|
||||
```
|
||||
|
||||
Confirm both are running:
|
||||
|
||||
```bash
|
||||
systemctl status screen-brightness.service brightness-api.service
|
||||
curl -s http://127.0.0.1:8765/state
|
||||
```
|
||||
|
||||
### 5.2 Overlay (session app)
|
||||
|
||||
Install the overlay binary and its **Wayland layer-shell** dependency:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y gir1.2-gtklayershell-0.1
|
||||
sudo install -m 755 brightness-overlay.py /usr/local/bin/brightness-overlay.py
|
||||
```
|
||||
|
||||
The overlay will be started by the Weston session script (below); no desktop autostart is needed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Weston session: Chromium kiosk + overlay
|
||||
|
||||
Create a session that starts **Weston** with a custom script that launches **Chromium in kiosk mode** and the **brightness overlay**. When Chromium exits, you can either leave the session as-is or relaunch Chromium so the user cannot use anything else.
|
||||
|
||||
### 6.1 Kiosk URL and Chromium flags
|
||||
|
||||
Choose the URL your kiosk should open (e.g. your bridge or portal). Example:
|
||||
|
||||
- `https://your-portal.example.com/`
|
||||
- Or a local page: `file:///usr/share/kiosk/index.html`
|
||||
|
||||
Chromium flags used here:
|
||||
|
||||
- `--kiosk` — Fullscreen, no browser chrome.
|
||||
- `--noerrdialogs` — No error dialogs.
|
||||
- `--disable-infobars` — No "Chrome is being controlled" bar.
|
||||
- `--no-first-run` — Skip first-run experience.
|
||||
- `--disable-session-crashed-bubble` — No "Restore pages?".
|
||||
- `--start-fullscreen`
|
||||
- Optional: `--autoplay-policy=no-user-gesture-required` if you need autoplay.
|
||||
|
||||
### 6.2 Weston startup script
|
||||
|
||||
Create a script that Weston will run as the session. It starts the overlay and Chromium; optionally restarts Chromium when it exits.
|
||||
|
||||
```bash
|
||||
sudo install -d /usr/local/share/weston
|
||||
sudo nano /usr/local/share/weston/kiosk-session.sh
|
||||
```
|
||||
|
||||
Contents (replace `KIOSK_URL` with your URL):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Weston session: brightness overlay + Chromium kiosk.
|
||||
# Run in Weston's compositor process so WAYLAND_DISPLAY is set.
|
||||
|
||||
KIOSK_URL="${KIOSK_URL:-https://example.com/}"
|
||||
|
||||
# Start overlay (uses GtkLayerShell; stays on top)
|
||||
/usr/local/bin/brightness-overlay.py &
|
||||
OVERLAY_PID=$!
|
||||
|
||||
# Optional: restart Chromium when it exits (user cannot escape to desktop)
|
||||
while true; do
|
||||
chromium-browser \
|
||||
--kiosk \
|
||||
--noerrdialogs \
|
||||
--disable-infobars \
|
||||
--no-first-run \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-restore-session-state \
|
||||
--start-fullscreen \
|
||||
"$KIOSK_URL"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# If not using the loop, just run chromium-browser once (user sees Weston when it closes).
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
sudo chmod +x /usr/local/share/weston/kiosk-session.sh
|
||||
```
|
||||
|
||||
Set the URL (e.g. via environment or by editing the script):
|
||||
|
||||
```bash
|
||||
# Example: set default URL
|
||||
echo 'KIOSK_URL="https://your-portal.example.com/"' | sudo tee /etc/default/weston-kiosk
|
||||
# Then in kiosk-session.sh, source it: . /etc/default/weston-kiosk 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 6.3 Launch Weston with the session script
|
||||
|
||||
You need Weston to start with your script instead of the default shell. Options:
|
||||
|
||||
**Option A — Weston and shell (common):**
|
||||
Start Weston so it runs a shell that runs your script:
|
||||
|
||||
```bash
|
||||
weston -- -- /usr/local/share/weston/kiosk-session.sh
|
||||
```
|
||||
|
||||
**Option B — systemd user or getty replacement:**
|
||||
Create a systemd service that runs on the console (e.g. `tty1`) and executes:
|
||||
|
||||
```bash
|
||||
/usr/bin/weston --tty=1 -- -- /usr/local/share/weston/kiosk-session.sh
|
||||
```
|
||||
|
||||
That way the kiosk starts automatically at boot (see [Autologin and starting Weston at boot](#7-autologin-and-starting-weston-at-boot)).
|
||||
|
||||
---
|
||||
|
||||
## 7. Autologin and starting Weston at boot
|
||||
|
||||
To have the device boot straight into the kiosk (no login):
|
||||
|
||||
1. **Autologin** — Configure getty or your login manager so the kiosk user is logged in on `tty1` (e.g. Raspberry Pi OS "Auto login" or `getty@tty1` override with `ExecStart=-/sbin/agetty --autologin pi --noclear %I $TERM`).
|
||||
2. **Run Weston from the autologin shell** — In the kiosk user's `.bash_profile` or `.profile`, start Weston only when on the console (so you don't start a second session over SSH):
|
||||
|
||||
```bash
|
||||
# In ~/.profile or ~/.bash_profile (for user e.g. pi)
|
||||
if [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
exec /usr/bin/weston --tty=1 -- -- /usr/local/share/weston/kiosk-session.sh
|
||||
fi
|
||||
```
|
||||
|
||||
Then reboot; the kiosk should start after autologin.
|
||||
|
||||
Alternatively, use a **systemd service** that runs on boot and starts Weston on `tty1` (replacing the getty for `tty1`). That avoids depending on shell profile and gives a single place to manage the kiosk.
|
||||
|
||||
---
|
||||
|
||||
## 8. Weston background (optional)
|
||||
|
||||
Weston can show a background image or colour. It is visible when Chromium is not fullscreen (e.g. at session start, or if Chromium exits and you are not auto-restarting it).
|
||||
|
||||
Configure in `weston.ini` (e.g. `~/.config/weston.ini` or `/etc/xdg/weston/weston.ini`). In the `[shell]` section:
|
||||
|
||||
- **Solid colour:** `background-color=0xff002244` (ARGB hex)
|
||||
- **Image:** `background-image=/path/to/your/wallpaper.png`
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary checklist
|
||||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| 1 | Use Raspberry Pi OS **Lite** (or convert to it). |
|
||||
| 2 | Install reTerminal DM drivers (backlight, sensor, buzzer). |
|
||||
| 3 | Install `weston`, `chromium-browser`, and (for overlay) `gir1.2-gtklayershell-0.1`. |
|
||||
| 4 | Install **screen-brightness** and **brightness-api** as systemd services. |
|
||||
| 5 | Install **brightness-overlay.py** and start it from the Weston session script. |
|
||||
| 6 | Create **kiosk-session.sh** that starts overlay + Chromium (with optional Chromium restart loop). |
|
||||
| 7 | Start Weston at boot (autologin + `.profile` or systemd) with `kiosk-session.sh`. |
|
||||
|
||||
After this, the device runs **Lite + Wayland** with no full GUI, only Chromium in kiosk mode and the brightness overlay on top. Your existing screen brightness control (daemon + API + overlay) continues to work; the overlay uses GtkLayerShell so it stays above Chromium.
|
||||
|
||||
---
|
||||
|
||||
## 10. Optional: hardening
|
||||
|
||||
- **Restrict TTYs** — Disable or limit getty on other TTYs so users cannot switch to a shell (e.g. mask `getty@tty2` … `getty@tty6` or restrict access).
|
||||
- **Kiosk user** — Use a dedicated user (e.g. `kiosk`) with minimal privileges and no password or a random one; autologin that user on `tty1`.
|
||||
- **Read-only root** — For maximum lock-down, consider a read-only root filesystem and overlayfs for temporary writes (document separately).
|
||||
- **Network** — Restrict outbound traffic if the kiosk only needs to reach specific hosts.
|
||||
|
||||
---
|
||||
|
||||
## 11. References
|
||||
|
||||
- [Weston](https://wayland.freedesktop.org/docs/html/) — Wayland reference compositor.
|
||||
- [Raspberry Pi OS](https://www.raspberrypi.com/software/) — Lite image.
|
||||
- [Seeed reTerminal DM](https://wiki.seeedstudio.com/reterminal-dm/) — Hardware and drivers.
|
||||
- [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) — Brightness daemon, API, and overlay installation and configuration.
|
||||
@@ -18,8 +18,9 @@ Devices plugged into the same network as **eth1** (e.g. reTerminals with network
|
||||
## What you need on the LXC
|
||||
|
||||
1. **DHCP server** on eth1 only (e.g. **dnsmasq**), handing out addresses in e.g. `10.20.50.100`–`10.20.50.200` and advertising the TFTP server (next-server = LXC’s eth1 IP).
|
||||
2. **TFTP server** (dnsmasq can provide this) with **TFTP root** containing Raspberry Pi 4 / CM4 boot files.
|
||||
3. **IP forwarding** and **NAT** (nftables or iptables) so traffic from `10.20.50.0/24` is masqueraded out **eth0**.
|
||||
2. **DNS server** on eth1 (dnsmasq): static name **file.server** → eth1 IP so scripts can use `http://file.server/...`; other queries forwarded upstream. See [DNSMASQ-DNS-FILESERVER.md](DNSMASQ-DNS-FILESERVER.md).
|
||||
3. **TFTP server** (dnsmasq can provide this) with **TFTP root** containing Raspberry Pi 4 / CM4 boot files.
|
||||
4. **IP forwarding** and **NAT** (nftables or iptables) so traffic from `10.20.50.0/24` is masqueraded out **eth0**.
|
||||
|
||||
## One-time setup (inside the LXC)
|
||||
|
||||
@@ -40,15 +41,21 @@ bash /path/to/setup-network-boot-on-lxc.sh
|
||||
|
||||
The script will:
|
||||
|
||||
- Install **dnsmasq** (DHCP + TFTP).
|
||||
- Configure dnsmasq to listen only on **eth1**, with a DHCP range and TFTP root.
|
||||
- Install **dnsmasq** (DHCP + TFTP + DNS).
|
||||
- Configure dnsmasq to listen only on **eth1**, with a DHCP range, TFTP root, and DNS (including **file.server** → eth1).
|
||||
- Create `/srv/tftpboot` and **fetch Raspberry Pi 4 boot files from GitHub** (raspberrypi/firmware, `boot/` folder) if not already present.
|
||||
- Enable **IPv4 forwarding** and **NAT** (nftables) so clients on eth1 use eth0 for internet.
|
||||
- Enable and start the **dnsmasq** service.
|
||||
|
||||
## Proxmox: adding eth1 to the LXC
|
||||
|
||||
If you create the container by hand or want a second interface:
|
||||
Use the deploy script with **`DEPLOY_LXC_LAN_BRIDGE`** and **`DEPLOY_LXC_LAN_SUBNET`** so the LXC is created with eth1 (LAN) from the start:
|
||||
|
||||
```bash
|
||||
DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
|
||||
Or add a second interface to an existing container by hand:
|
||||
|
||||
1. On the **Proxmox host**, add a second network device to the container, e.g.:
|
||||
```bash
|
||||
@@ -65,6 +72,37 @@ If you create the container by hand or want a second interface:
|
||||
|
||||
Your current LXC already has eth0 (10.130.60.141) and eth1 (10.20.50.1); the setup script only adds DHCP, TFTP, and NAT.
|
||||
|
||||
### Changing the LAN subnet
|
||||
|
||||
When you deploy with **`DEPLOY_LXC_LAN_SUBNET`** (e.g. `10.100.1.1/24`), the deploy script writes **`/opt/cm4-provisioning/lan-subnet.conf`** inside the LXC with `LAN_GW`, `LAN_CIDR`, and the DHCP range. All LXC services use this file:
|
||||
|
||||
- **dnsmasq** (DHCP range and, via the toggle script, TFTP next-server)
|
||||
- **nftables/iptables** (NAT source subnet)
|
||||
- **toggle-network-boot-dhcp.sh** (option 66/67 next-server)
|
||||
|
||||
So changing `DEPLOY_LXC_LAN_SUBNET` and **re-running the deploy script** updates `lan-subnet.conf`. To apply the new subnet to dnsmasq and NAT, **re-run the setup script** after redeploying:
|
||||
|
||||
```bash
|
||||
./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@<LXC-IP>
|
||||
```
|
||||
|
||||
Then run **toggle enable** again if you use network boot: `ssh root@<LXC-IP> /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable`
|
||||
|
||||
### Extra LAN IPs and VLAN (eth1.40)
|
||||
|
||||
The setup script also configures **extra IPs on eth1** and a **VLAN interface** so the LXC can serve multiple subnets and provide internet (NAT) to all of them:
|
||||
|
||||
| Address / interface | Purpose |
|
||||
|--------------------|--------|
|
||||
| **Primary** (e.g. `10.20.50.1/24`) | Set at deploy; used by dnsmasq for DHCP/TFTP/DNS |
|
||||
| **192.168.30.1/24** | Extra LAN on eth1 |
|
||||
| **192.168.127.1/24** | Extra LAN on eth1 |
|
||||
| **eth1.40** **192.168.0.1/24** | VLAN 40 on eth1 |
|
||||
|
||||
- Config is persisted in **`/etc/network/interfaces.d/70-cm4-extra-lan`** (installed when you run `setup-network-boot-on-lxc.sh`).
|
||||
- **NAT** is applied to all four: primary LAN, 192.168.30.0/24, 192.168.127.0/24, and 192.168.0.0/24 (VLAN 40), so clients on any of these subnets get internet via eth0.
|
||||
- For **VLAN 40** to receive tagged traffic, the Proxmox bridge connected to eth1 (e.g. vmbr1) must either be a trunk that passes VLAN 40, or you use a dedicated bridge (e.g. vmbr1.40) and attach the container to it as a second interface; the script creates eth1.40 inside the LXC for the in-container VLAN case.
|
||||
|
||||
## After setup: reTerminal network boot
|
||||
|
||||
1. Set the reTerminal **boot order** to try eMMC first, then network (e.g. `BOOT_ORDER=0xf21`): use the dashboard **Update EEPROM** when the device is connected via USB boot, or set manually (usbboot recovery / `rpi-eeprom-config` on device). Not set by first-boot.
|
||||
|
||||
63
emmc-provisioning/docs/PROXMOX-HOST-COMPARISON.md
Normal file
63
emmc-provisioning/docs/PROXMOX-HOST-COMPARISON.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Proxmox host comparison: working vs new
|
||||
|
||||
Quick reference from comparing **working** (10.130.60.224) and **new** (100.106.128.36) Proxmox hosts.
|
||||
|
||||
## Same on both
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| `/opt/cm4-provisioning/env` | Same (GOLDEN_IMAGE, RPIBOOT_DIR, EMMC_SIZE_BYTES=8589934592, SHRINK_BACKUP, etc.) |
|
||||
| `/etc/cm4-provisioning/enabled` | Present (provisioning enabled) |
|
||||
| udev rules `89-cm4-boot-mode-permissions.rules` | Same (MODE="0666" for 2b8e, 0a5c:2711) |
|
||||
| udev rules `90-cm4-boot-mode.rules` | Same (trigger cm4-flash-trigger.sh on add) |
|
||||
| `/usr/local/bin/cm4-flash-trigger.sh` | Same (starts cm4-flash.service) |
|
||||
| `cm4-flash.service` | Same (ExecStartPre=sleep 5, TimeoutStartSec=7200) |
|
||||
| `cm4-build-cloudinit.path`, `cm4-shrink.path` + services | Same |
|
||||
| `build-cloudinit-image.sh`, `run-shrink-on-host.sh`, `fix-gadget-bootcode-on-host.sh` | Same (md5 match) |
|
||||
|
||||
## Differences
|
||||
|
||||
### 1. `/opt/usbboot/mass-storage-gadget64/bootcode4.bin` — **fixed**
|
||||
|
||||
- **Working:** Present (105984 bytes). rpiboot needs this or it prints "No 'bootcode' files found".
|
||||
- **New:** Was missing; fixed by copying from working host. Verify: `ls -la /opt/usbboot/mass-storage-gadget64/bootcode4.bin` on new.
|
||||
|
||||
### 2. `/opt/cm4-provisioning/flash-emmc-on-connect.sh`
|
||||
|
||||
- **Working:** Older version (14421 bytes, md5 `dbac0bc2...`).
|
||||
- **New:** Newer version (15449 bytes, md5 `6081bda7...`) with longer device wait (60s/90s), relaxed size check (50–120%), and extra diagnostics on "No suitable block device".
|
||||
|
||||
**Recommendation:** Copy the repo script to the **working** host so both use the same version:
|
||||
|
||||
```bash
|
||||
scp emmc-provisioning/host/flash-emmc-on-connect.sh root@10.130.60.224:/opt/cm4-provisioning/
|
||||
```
|
||||
|
||||
### 3. `/var/lib/cm4-provisioning/golden.img`
|
||||
|
||||
- **Working:** Present (symlink to a backup image). Required for **Deploy** (writing image to device).
|
||||
- **New:** Missing. Backup works; Deploy will show "Golden image not found" until you set one (dashboard “Set as golden” or `scp` an image to the host as `golden.img`).
|
||||
|
||||
### 4. `/opt/cm4-provisioning/dashboard/` (host only)
|
||||
|
||||
- **Working:** Directory exists (owner 1000:1000). Dashboard normally runs in the **LXC**, so this may be leftover or unused on the host.
|
||||
- **New:** No dashboard dir on host. No action needed unless you run the dashboard on the host.
|
||||
|
||||
### 5. Extra files in `/var/lib/cm4-provisioning/` on working
|
||||
|
||||
- **Working:** `build_cloudinit_status.json`, `cloudinit_templates.json`, `first_boot_status.json` (written by dashboard/LXC when using cloud-init build).
|
||||
- **New:** Not present yet. They appear when you use the dashboard from the LXC; not required for flash/backup.
|
||||
|
||||
## Checklist for new host (100.106.128.36)
|
||||
|
||||
- [x] `bootcode4.bin` in `/opt/usbboot/mass-storage-gadget64/` (copied from working)
|
||||
- [x] Same udev rules, trigger, and systemd units
|
||||
- [x] Same env. `EMMC_SIZE_BYTES` is optional; device detection is dynamic (any new block device after rpiboot is accepted for 8/16/32 GB CM4).
|
||||
- [ ] Set `golden.img` for Deploy (copy image or use dashboard “Set as golden” from a backup)
|
||||
- [x] `flash-emmc-on-connect.sh` is the updated version (longer wait, diagnostics)
|
||||
|
||||
## If flash still fails on new host
|
||||
|
||||
1. Check flash.log: `ssh root@100.106.128.36 'tail -80 /var/lib/cm4-provisioning/flash.log'` — the script now logs "Current block devices" and sizes when no device is found.
|
||||
2. Ensure eMMC disable jumper is set and you use the USB slave port; unplug and replug.
|
||||
3. Optional: increase udev/systemd delay (e.g. `ExecStartPre=/bin/sleep 10` in `cm4-flash.service`) if the device is slow to enumerate.
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The auto-flash **runs on the Proxmox host** (where the USB device appears). The **LXC** holds the dashboard and shares the **golden image** directory with the host.
|
||||
|
||||
**New deployment:** For a clear step-by-step guide to deploy on a **new** Proxmox instance, see **[DEPLOY-NEW-PROXMOX.md](DEPLOY-NEW-PROXMOX.md)**.
|
||||
|
||||
## One-command deploy
|
||||
|
||||
From your repo, a single run deploys **all** host and LXC files (scripts, systemd units, udev, dashboard):
|
||||
@@ -12,6 +14,8 @@ From your repo, a single run deploys **all** host and LXC files (scripts, system
|
||||
|
||||
Optional env: `CM4_BACKUPS_HOST_PATH=/path`, `DEPLOY_ROOTFS_STORAGE=local-lvm`, `DEPLOY_LXC_ROOT_PASSWORD=secret` (set root password in LXC and enable SSH), `DEPLOY_LXC_SSH_KEY=/path/to/pub` (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub` — copied to LXC root so you can `ssh root@<LXC-IP>`).
|
||||
|
||||
**Network (WAN / LAN):** `DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default or e.g. `192.168.1.10/24`), `DEPLOY_LXC_LAN_BRIDGE=vmbr1` (if set, add eth1 as LAN), `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` (LXC IP on LAN; default `10.20.50.1/24` when LAN bridge is set). Example: `DEPLOY_LXC_WAN_BRIDGE=vmbr0 DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./scripts/deploy-to-proxmox.sh root@host`.
|
||||
|
||||
The script **finds the container by hostname `cm4-provisioning`** (any existing ID). If none exists, it **creates a new LXC with the next available ID**. So you can redeploy repeatedly without assuming a fixed ID like 201.
|
||||
|
||||
## What is deployed
|
||||
@@ -206,11 +210,54 @@ Or copy `scripts/monitor-from-host.sh` to the host and run `./monitor-from-host.
|
||||
|
||||
2. **Unplug and replug the USB** – udev runs the trigger only when the device is *added*. Unplug the reTerminal USB (keep it in boot mode), then plug it back in. The trigger will run the script and rpiboot; when the eMMC is exposed, the portal shows "Device connected" with Backup/Deploy.
|
||||
|
||||
3. **If rpiboot fails** – Check on the host: `ssh root@10.130.60.224 'tail -30 /var/lib/cm4-provisioning/flash.log'` (rpiboot stderr is appended there). Try unplug/replug again. To see the exact rpiboot error: `ssh root@10.130.60.224 '/opt/usbboot/rpiboot -d /opt/usbboot/mass-storage-gadget64'` (device connected; Ctrl+C to stop). Run `scripts/monitor-from-host.sh` for a full snapshot.
|
||||
3. **If rpiboot fails** ("rpiboot failed or no device connected"):
|
||||
- **Check flash.log on the host** (rpiboot runs there; the log has the real error):
|
||||
`ssh root@YOUR_PROXMOX_HOST 'tail -50 /var/lib/cm4-provisioning/flash.log'`
|
||||
- **Quick diagnostic:**
|
||||
`ssh root@YOUR_PROXMOX_HOST 'bash -s' < emmc-provisioning/scripts/check-usb-on-host.sh`
|
||||
This shows whether the device is seen (lsusb), status, and last lines of flash.log.
|
||||
- **Ensure device is in boot mode:** eMMC disable jumper set, use the **USB slave** port (not host port). Unplug and replug after setting jumper.
|
||||
- **Run rpiboot manually** (device connected; Ctrl+C to stop):
|
||||
`ssh root@YOUR_PROXMOX_HOST '/opt/usbboot/rpiboot -v -d /opt/usbboot/mass-storage-gadget64'`
|
||||
The last line before exit is usually the error (e.g. "No device found", "Unable to open device").
|
||||
- Run `scripts/monitor-from-host.sh root@YOUR_PROXMOX_HOST` for a full snapshot.
|
||||
|
||||
**Manual usbboot test** (to verify rpiboot and the CM4 without the full flash flow):
|
||||
|
||||
1. **On the Proxmox host** (where USB is connected), with the reTerminal in **boot mode** (eMMC disable jumper set, USB slave port connected):
|
||||
```bash
|
||||
/opt/usbboot/rpiboot -v -d /opt/usbboot/mass-storage-gadget64
|
||||
```
|
||||
2. You should see lines like: `Device located successfully`, `Loading: .../bootcode4.bin`, `Sending bootcode.bin`. When the device switches to mass storage, rpiboot exits and a new `/dev/sdX` may appear (check with `lsblk` in another terminal). Press **Ctrl+C** to stop rpiboot at any time.
|
||||
3. **From your machine** (with device already connected to the host):
|
||||
```bash
|
||||
./emmc-provisioning/scripts/test-usbboot-on-host.sh root@YOUR_PROXMOX_HOST
|
||||
```
|
||||
Optional: run with a timeout so it doesn’t wait forever: `TIMEOUT=60 ./emmc-provisioning/scripts/test-usbboot-on-host.sh root@YOUR_PROXMOX_HOST`
|
||||
|
||||
**"libusb_bulk_transfer sent 0 bytes; returned -7" / "Failed to write correct length"** (device found, then transfer fails in a loop):
|
||||
|
||||
This is a known USB timing/controller issue with CM4 and rpiboot ([raspberrypi/usbboot#36](https://github.com/raspberrypi/usbboot/issues/36)). Try in order:
|
||||
|
||||
1. **Use a USB 2.0 port** on the Proxmox host (not USB 3.0). Many reports say USB 2.0 is more reliable for rpiboot.
|
||||
2. **Avoid udev fighting with manual rpiboot:** Temporarily disable the provisioning trigger so only your manual rpiboot runs:
|
||||
`ssh root@HOST 'mv /etc/cm4-provisioning/enabled /etc/cm4-provisioning/enabled.bak'`
|
||||
Run rpiboot, then re-enable:
|
||||
`ssh root@HOST 'mv /etc/cm4-provisioning/enabled.bak /etc/cm4-provisioning/enabled'`
|
||||
3. **Try a different USB port and cable** on the host; unplug/replug and retry.
|
||||
4. **Add a delay** so the device is ready before transfer:
|
||||
`rpiboot -v -d /opt/usbboot/mass-storage-gadget64 -m 2000`
|
||||
(`-m` is microseconds between device checks; 2000 = 2 ms.)
|
||||
5. **Reboot the Proxmox host** and try again (USB controller state can get stuck).
|
||||
6. **Find a USB 2.0 port:** On the host run `lsusb -t` — look for the BCM2711 device; the tree shows which controller (e.g. "xHCI" = USB 3, "ehci" = USB 2). Try a port that is under an **ehci** or **ohci** controller, or a black (non‑blue) physical port.
|
||||
7. **Try a powered USB 2.0 hub** between host and reTerminal (some hosts work only through a hub).
|
||||
8. If you have the **working host** (10.130.60.224), try the same reTerminal there; if it works there, the difference is host USB controller or port.
|
||||
|
||||
3b. **"No suitable block device after rpiboot"** – rpiboot ran but no new block device was seen. Detection is **dynamic**: any block device that appears after rpiboot (not present before) is used, so 8/16/32 GB CM4 work without setting eMMC size. **Check on host:** `tail -80 /var/lib/cm4-provisioning/flash.log` — at the end you’ll see "Current block devices" and each `/dev/sdX` with size. **Causes:** (1) Device didn’t switch to mass storage (try unplug/replug, keep eMMC disable jumper set). (2) udev slow — try again; the script waits up to 90s for the device.
|
||||
|
||||
4. **"No 'bootcode' files found in mass-storage-gadget64"** – Usually because `bootfiles.bin` is a **broken symlink** (e.g. `-> ../firmware/bootfiles.bin`) and that target doesn’t exist. **Fix on host:** run `scripts/fix-gadget-bootcode-on-host.sh` on the host (it removes the symlink and extracts `bootcode4.bin` from the installed rpiboot binary). From your machine: `ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh`. **Alternative:** repopulate the gadget dir with `./scripts/populate-gadget-on-host.sh root@10.130.60.224`, or full reinstall with `./scripts/build-and-deploy-usbboot-to-host.sh root@10.130.60.224`. Then verify: `ls -la /opt/usbboot/mass-storage-gadget64/` (should list a real `bootcode4.bin` or `bootfiles.bin`, plus `boot.img`, `config.txt`).
|
||||
|
||||
4. **Clear stuck error in portal** – If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for reTerminal in boot mode or network.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device.
|
||||
4. **Clear stuck error in portal** – If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for Device in USB boot mode.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device.
|
||||
|
||||
5. **"PiShrink not installed" when clicking Shrink/Compress** – Shrink and Compress run **on the host**, not in the LXC. Install PiShrink on the host: `ssh root@HOST 'bash -s' < emmc-provisioning/scripts/install-pishrink-on-host.sh`. Ensure deploy has been run so the host has `run-shrink-on-host.sh` and `cm4-shrink.path` enabled (`systemctl status cm4-shrink.path`).
|
||||
|
||||
|
||||
276
emmc-provisioning/docs/SCREEN-BRIGHTNESS-MANUAL-SETUP.md
Normal file
276
emmc-provisioning/docs/SCREEN-BRIGHTNESS-MANUAL-SETUP.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Screen Brightness Service — Manual Setup on Existing Device
|
||||
|
||||
This guide describes how to install and configure the **screen-brightness** daemon on an existing reTerminal DM (e.g. a device that was not provisioned with the first-boot flow that includes step 14). The daemon controls LCD backlight from the ambient light sensor and raises brightness when the buzzer is active (alarm), with behaviour aligned to bridge display regulations.
|
||||
|
||||
**See also:** [SCREEN-BRIGHTNESS-REGULATIONS.md](SCREEN-BRIGHTNESS-REGULATIONS.md) for the regulatory references.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
- **Hardware:** reTerminal DM (Raspberry Pi CM4-based) with built-in light sensor and buzzer.
|
||||
- **OS:** Raspberry Pi OS (or compatible) with:
|
||||
- Python 3
|
||||
- Sysfs nodes present (see [Verify hardware](#2-verify-hardware-below)).
|
||||
|
||||
No extra Python packages are required (stdlib only).
|
||||
|
||||
---
|
||||
|
||||
## 2. Verify hardware
|
||||
|
||||
On the device, confirm that the backlight, buzzer, and light sensor are available:
|
||||
|
||||
```bash
|
||||
# Backlight (values 0–5)
|
||||
cat /sys/class/backlight/lcd_backlight/brightness
|
||||
cat /sys/class/backlight/lcd_backlight/max_brightness
|
||||
|
||||
# Buzzer (0 = off, 1 = on)
|
||||
cat /sys/class/leds/usr-buzzer/brightness
|
||||
|
||||
# Ambient light (lux)
|
||||
cat /sys/bus/iio/devices/iio:device0/in_illuminance_input
|
||||
```
|
||||
|
||||
If any of these paths are missing, install the reTerminal DM drivers first (Seeed wiki: [reTerminal DM Getting Started](https://wiki.seeedstudio.com/reterminal-dm/)).
|
||||
|
||||
---
|
||||
|
||||
## 3. Install the service (manual copy)
|
||||
|
||||
### 3.1 Create directories and copy files
|
||||
|
||||
Run as **root** (or with `sudo`):
|
||||
|
||||
```bash
|
||||
# Script
|
||||
install -m 755 screen-brightness.py /usr/local/bin/screen-brightness.py
|
||||
|
||||
# Systemd unit
|
||||
install -m 644 screen-brightness.service /etc/systemd/system/screen-brightness.service
|
||||
|
||||
# Optional: default config (skip if you already have vessel-specific /etc/screen-brightness.conf)
|
||||
install -m 644 screen-brightness.conf /etc/screen-brightness.conf
|
||||
```
|
||||
|
||||
Use the files from this repo:
|
||||
|
||||
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.py`
|
||||
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.service`
|
||||
- `emmc-provisioning/cloud-init/fileserver/screen-brightness.conf`
|
||||
|
||||
Copy them to the device first (e.g. via SCP, USB stick, or the deployment script below), then run the `install` commands on the device.
|
||||
|
||||
### 3.2 Runtime directory and tmpfiles
|
||||
|
||||
Ensure the daemon’s runtime directory exists and is recreated after reboot:
|
||||
|
||||
```bash
|
||||
mkdir -p /run/screen-brightness
|
||||
|
||||
# Create tmpfiles.d rule so the directory exists after reboot
|
||||
cat > /etc/tmpfiles.d/screen-brightness.conf << 'EOF'
|
||||
d /run/screen-brightness 0755 root root -
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3.3 Enable and start
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable screen-brightness.service
|
||||
systemctl start screen-brightness.service
|
||||
```
|
||||
|
||||
Check status and logs:
|
||||
|
||||
```bash
|
||||
systemctl status screen-brightness.service
|
||||
journalctl -u screen-brightness.service -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Install using the deployment script
|
||||
|
||||
A helper script can install or update the service from **either** a local directory (repo clone/USB) **or** a URL (e.g. your file server). The script lives in the repo at:
|
||||
|
||||
`emmc-provisioning/scripts/deploy-screen-brightness-to-device.sh`
|
||||
|
||||
### 4.1 On the device (files already on the device)
|
||||
|
||||
If you have the three files in a directory on the reTerminal DM (e.g. copied from USB or repo):
|
||||
|
||||
```bash
|
||||
cd /path/to/dir # directory containing screen-brightness.py, .service, .conf
|
||||
sudo ./deploy-screen-brightness-to-device.sh
|
||||
```
|
||||
|
||||
The script will install the files, create the tmpfiles.d rule, and start/enable the service. It will **not** overwrite an existing `/etc/screen-brightness.conf` (so vessel-specific tuning is preserved).
|
||||
|
||||
### 4.2 On the device (download from URL)
|
||||
|
||||
If your file server serves the three files (e.g. from the provisioning portal), pass the base URL as the **first argument** (recommended — works even when `sudo` does not preserve environment variables):
|
||||
|
||||
```bash
|
||||
sudo ./deploy-screen-brightness-to-device.sh "http://file.server:5000/files"
|
||||
```
|
||||
|
||||
Alternatively, if your `sudo` preserves the environment:
|
||||
|
||||
```bash
|
||||
sudo -E BOOTSTRAP_URL="http://file.server:5000/files" ./deploy-screen-brightness-to-device.sh
|
||||
```
|
||||
|
||||
The script will download `screen-brightness.py`, `screen-brightness.service`, and `screen-brightness.conf` from that base URL and show progress per file.
|
||||
|
||||
### 4.3 From a host (deploy via SSH)
|
||||
|
||||
From your laptop or build host, with the repo checked out and SSH access to the device:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo/emmc-provisioning/scripts
|
||||
./deploy-screen-brightness-to-device.sh root@192.168.1.100
|
||||
```
|
||||
|
||||
The script will copy the files from `../cloud-init/fileserver/` and run the install steps over SSH.
|
||||
|
||||
### 4.4 From a host via a jump server (bastion)
|
||||
|
||||
If the reTerminal DM is only reachable through a jump host, set `SSH_JUMP_HOST` and pass the device as the first argument:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo/emmc-provisioning/scripts
|
||||
SSH_JUMP_HOST="user@jump.example.com" ./deploy-screen-brightness-to-device.sh pi@10.0.0.5
|
||||
```
|
||||
|
||||
The script uses OpenSSH’s `-J` / `ProxyJump` so `ssh` and `scp` go through the jump host to the device.
|
||||
|
||||
**Alternative:** put the jump in your SSH config so no env var is needed:
|
||||
|
||||
```
|
||||
# ~/.ssh/config
|
||||
Host reterminal-guard
|
||||
HostName 10.0.0.5
|
||||
User pi
|
||||
ProxyJump user@jump.example.com
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
./deploy-screen-brightness-to-device.sh reterminal-guard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration
|
||||
|
||||
- **Config file:** `/etc/screen-brightness.conf` (INI format, optional).
|
||||
If the file is missing, the daemon uses built-in defaults.
|
||||
- **Reload config without restart:**
|
||||
`sudo systemctl reload screen-brightness`
|
||||
- **Override (navigator preset):**
|
||||
- Force a fixed level (1–5):
|
||||
`echo 3 | sudo tee /run/screen-brightness/override`
|
||||
- Return to automatic (ambient + buzzer):
|
||||
`echo auto | sudo tee /run/screen-brightness/override`
|
||||
|
||||
See comments in `screen-brightness.conf` for lux thresholds and level mapping (NIGHT / DUSK / DAY).
|
||||
|
||||
### 5.1 Crew brightness overlay (manual control)
|
||||
|
||||
To satisfy the requirement that the navigator can manually control brightness (MSC.191(79) §8.1.2), you can use the **brightness overlay**: a small panel that sits on top of the kiosk (Wayland layer-shell or X11) with buttons **1** **2** **3** **4** **5** and **Auto**.
|
||||
|
||||
- **brightness-api.service** — HTTP API on `127.0.0.1:8765`. Accepts `POST /brightness` with `level=1|2|3|4|5|auto`. Runs as root and writes `/run/screen-brightness/override`.
|
||||
- **brightness-overlay.py** — Gtk3 overlay (same pattern as five-tap-close-chromium). Starts from session autostart; calls the API when a button is tapped.
|
||||
|
||||
If step 14 installed them, the overlay appears at the **bottom-right** after login. To install manually on an existing device:
|
||||
|
||||
```bash
|
||||
# API (run as root)
|
||||
sudo install -m 755 brightness-api.py /usr/local/bin/
|
||||
sudo install -m 644 brightness-api.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload && sudo systemctl enable --now brightness-api.service
|
||||
|
||||
# Overlay (run as pi, autostart)
|
||||
install -m 755 brightness-overlay.py ~/brightness-overlay.py
|
||||
# Then add brightness-overlay.desktop to ~/.config/autostart/ (Exec path = your $HOME/brightness-overlay.py)
|
||||
```
|
||||
|
||||
Dependencies: Python 3, PyGObject (Gtk3). On Wayland, `gir1.2-gtklayershell-0.1` for the overlay layer.
|
||||
|
||||
---
|
||||
|
||||
## 6. State and dashboard integration
|
||||
|
||||
The daemon writes a state file for other services (e.g. your alarm dashboard):
|
||||
|
||||
```bash
|
||||
cat /run/screen-brightness/state
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
mode=ALERT
|
||||
level=5
|
||||
lux=12
|
||||
buzzer=1
|
||||
override=0
|
||||
updated=1741266000
|
||||
```
|
||||
|
||||
Your dashboard can read this to show current brightness mode or to drive a “night mode” indicator.
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing buzzer and brightness
|
||||
|
||||
To confirm that the screen goes to full brightness when the buzzer is active and stays up during cooldown, run a short beep test on the device:
|
||||
|
||||
```bash
|
||||
# From the device (if you have the script in place)
|
||||
sudo ./test-buzzer-brightness.sh
|
||||
```
|
||||
|
||||
If the script is not on the device, fetch and run it from your file server (replace with your portal URL):
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://10.130.60.141:5000/files/first-boot/test-buzzer-brightness.sh" -o /tmp/test-buzzer-brightness.sh
|
||||
chmod +x /tmp/test-buzzer-brightness.sh
|
||||
sudo /tmp/test-buzzer-brightness.sh
|
||||
```
|
||||
|
||||
The script beeps four times. While it runs, the screen should go to maximum brightness (ALERT) and stay high for about 10 seconds after the last beep (COOLDOWN), then return to ambient level.
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|--------|--------|
|
||||
| Service fails to start | `journalctl -u screen-brightness.service -n 50`; confirm sysfs paths exist (step 2). |
|
||||
| Brightness never changes | Verify light sensor: `cat /sys/bus/iio/devices/iio:device0/in_illuminance_input` (value should change when you shade the sensor). |
|
||||
| Buzzer does not raise brightness | 1) Ensure service is running: `systemctl status screen-brightness.service`. 2) Use `test-buzzer-brightness.sh` (it uses the buzzer’s `max_brightness`, often 255). 3) Confirm buzzer sysfs updates while ON: `cat /sys/class/leds/usr-buzzer/brightness` (non-zero means ON), then `cat /run/screen-brightness/state` (mode=ALERT / COOLDOWN). |
|
||||
| No logs from brightness service | Check that the unit is active: `systemctl is-active screen-brightness.service`. Then `journalctl -u screen-brightness.service -f` (follow). If the service exits, check `journalctl -u screen-brightness.service -n 50` for Python or path errors. |
|
||||
| Brightness overlay not visible | 1) **Start it now:** from a terminal on the device (VNC or local), run: `python3 /home/pi/brightness-overlay.py` (or `~/brightness-overlay.py`). The panel should appear bottom-right; any errors will show in the terminal. 2) **After deploy:** autostart runs at login — log out and log back in so the overlay starts (it uses a 6 s delay). 3) **API required:** ensure `systemctl status brightness-api.service` is active; the overlay calls 127.0.0.1:8765. 4) **Dependencies:** need PyGObject (Gtk3); on Wayland, `gir1.2-gtklayershell-0.1` for the layer. |
|
||||
| Config change not applied | Run `sudo systemctl reload screen-brightness` (or restart the service). |
|
||||
|
||||
---
|
||||
|
||||
## 9. File locations (reference)
|
||||
|
||||
| Item | Path |
|
||||
|------|------|
|
||||
| Daemon script | `/usr/local/bin/screen-brightness.py` |
|
||||
| Systemd unit | `/etc/systemd/system/screen-brightness.service` |
|
||||
| Config | `/etc/screen-brightness.conf` |
|
||||
| Override (navigator) | `/run/screen-brightness/override` |
|
||||
| State (read-only) | `/run/screen-brightness/state` |
|
||||
| tmpfiles.d | `/etc/tmpfiles.d/screen-brightness.conf` |
|
||||
| Brightness API (crew control) | `/usr/local/bin/brightness-api.py` |
|
||||
| Brightness API unit | `/etc/systemd/system/brightness-api.service` |
|
||||
| Overlay script | `$HOME/brightness-overlay.py` |
|
||||
| Overlay autostart | `$HOME/.config/autostart/brightness-overlay.desktop` |
|
||||
163
emmc-provisioning/docs/SCREEN-BRIGHTNESS-REGULATIONS.md
Normal file
163
emmc-provisioning/docs/SCREEN-BRIGHTNESS-REGULATIONS.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Screen Brightness — Maritime Regulations and References
|
||||
|
||||
This document summarises the international regulations and standards that apply to **display brightness and dimming** on ship bridges, and how the **screen-brightness** daemon on the reTerminal DM is aligned with them. It is intended for vessel operators, integrators, and auditors.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Bridge equipment that presents navigation-related or alarm information on a display is subject to:
|
||||
|
||||
- **IMO** (International Maritime Organization) resolutions, which set performance standards.
|
||||
- **IEC** (International Electrotechnical Commission) and **IHO** (International Hydrographic Organization) standards, which give technical and test requirements.
|
||||
|
||||
The reTerminal DM screen-brightness daemon implements behaviour that satisfies the relevant clauses for:
|
||||
|
||||
- Adjustable brightness that is **never extinguished** (“not to extinction”).
|
||||
- A **navigator-resettable** brightness/contrast preset (override).
|
||||
- **High luminous intensity** for alarm/alert states.
|
||||
- **Night mode** that does not degrade crew night vision.
|
||||
- **Three operating modes** (NIGHT / DUSK / DAY) consistent with ECDIS-type display practice.
|
||||
|
||||
---
|
||||
|
||||
## 2. IMO MSC.191(79) — Navigational displays
|
||||
|
||||
**Full title:**
|
||||
*Performance Standards for the Presentation of Navigation-Related Information on Shipborne Navigational Displays*
|
||||
|
||||
**Adoption:** 6 December 2004
|
||||
|
||||
**Applicability:** Displays that present navigation-related information on the bridge (including ECDIS and other navigational displays). Often referenced for equipment installed after 1 July 2008.
|
||||
|
||||
**Source (external):**
|
||||
- [imorules.com – MSC.191(79)](https://www.imorules.com/MSCRES_191.79.html)
|
||||
- IMO official: MSC.191(79) in the IMO Publications catalogue.
|
||||
|
||||
### 2.1 §8.1 Display adjustment
|
||||
|
||||
| Requirement | Text (summary) | Implementation in screen-brightness |
|
||||
|-------------|-----------------|-------------------------------------|
|
||||
| Adjustability | Contrast and brightness shall be adjustable as applicable to the display technology. | Brightness is controlled in steps 1–5 from ambient sensor and/or override. |
|
||||
| Dimming | The display shall be capable of being **dimmed**. | NIGHT and DUSK modes set low backlight levels (1–2). |
|
||||
| Legibility | The range of control shall allow the display to be **legible under all ambient light conditions**. | Lux thresholds map to levels 1–5 so the display remains usable from night to bright day. |
|
||||
| **Not to extinction** | Implicit in “dimmed” and “legible under all conditions”: the display must not be turned off. | **Level 0 is never used.** Minimum level is 1 (“not to extinction”). |
|
||||
|
||||
### 2.2 §8.1.2 Preset / default
|
||||
|
||||
| Requirement | Text (summary) | Implementation in screen-brightness |
|
||||
|-------------|-----------------|-------------------------------------|
|
||||
| Reset to preset | The navigator shall be able to **reset** contrast and/or brightness to a **preset or default condition**. | Override file: `echo <1–5> \| sudo tee /run/screen-brightness/override` sets a fixed level; `echo auto` restores automatic (ambient + buzzer) behaviour. |
|
||||
|
||||
**Reference (section 8.1):**
|
||||
- [imorules.com – 8.1 Display adjustment](https://www.imorules.com/GUID-6B14EB44-F913-4C8F-8E36-F74CE6E8E3DE.html)
|
||||
|
||||
### 2.3 Position of the brightness control
|
||||
|
||||
**MSC.191(79), MSC.302(87), IHO S-52, and IEC 62288 do not specify where the brightness (or contrast) control must be located on the screen or in the user interface.** They only require that:
|
||||
|
||||
- Brightness be adjustable and dimmable.
|
||||
- The navigator be able to reset to a preset or default.
|
||||
|
||||
So the **position of the brightness control** (e.g. bottom-right overlay icon on the reTerminal DM) is an **implementation and design choice**, not a regulatory requirement. Placement can follow human-factors or vessel-specific guidelines (e.g. readily accessible, non-intrusive) as long as the above requirements are met.
|
||||
|
||||
---
|
||||
|
||||
## 3. IMO MSC.302(87) — Bridge alert management
|
||||
|
||||
**Full title:**
|
||||
*Adoption of Performance Standards for Bridge Alert Management*
|
||||
|
||||
**Adoption:** 17 May 2010
|
||||
|
||||
**Applicability:** Bridge Alert Management (BAM) and presentation of alerts on the bridge. Recommended for central alert management (CAM) and CAM human-machine interface (CAM-HMI) installed on or after 1 July 2014.
|
||||
|
||||
**Source (external):**
|
||||
- [imorules.com – MSC.302(87)](https://www.imorules.com/MSCRES_302.87.html)
|
||||
- IMO official: MSC.302(87) PDF in IMO Knowledge Centre.
|
||||
|
||||
### 3.1 Visual alarms and night vision
|
||||
|
||||
| Requirement | Text (summary) | Implementation in screen-brightness |
|
||||
|-------------|-----------------|-------------------------------------|
|
||||
| Night vision | Visual alarms and indicators shall **not interfere with night vision**. | In ambient mode, NIGHT/DUSK use low levels (1–2); transitions are smooth (one step per poll) to avoid sudden bright flashes. |
|
||||
| Dimming not to extinction | **Dimming facilities** shall be incorporated, **though not to extinction**. | Same as MSC.191(79): level 0 is never used; minimum is level 1. |
|
||||
| High luminous intensity for alerts | Supplemental **visual indicators** (e.g. for alarms) shall be of **high luminous intensity**. | When the **buzzer** is ON (alarm), backlight is set to **level 5 (maximum)** immediately, with no smooth stepping. |
|
||||
| Persistence after alarm | Alerts must remain visible/audible long enough for crew response. | After the buzzer stops, brightness is held at level 5 for a **cooldown period** (default 10 s), then stepped down; duration is configurable in `/etc/screen-brightness.conf`. |
|
||||
|
||||
**Reference:**
|
||||
- IMO MSC.302(87) Annex — Performance Standards for Bridge Alert Management (Modules A–D).
|
||||
- [imorules.com – MSC.302(87) Annex](https://www.imorules.com/MSCRES_302.87_ANN.html)
|
||||
|
||||
---
|
||||
|
||||
## 4. IHO S-52 and IEC 62288 — ECDIS and display presentation
|
||||
|
||||
**IHO S-52** (International Hydrographic Organization) specifies chart content and **display aspects** for ECDIS, including colour and luminance behaviour. **IEC 62288** implements and tests these and related IMO requirements for shipborne navigational displays.
|
||||
|
||||
### 4.1 Three colour / luminance modes
|
||||
|
||||
| Mode | Typical use | Implementation in screen-brightness |
|
||||
|------|-------------|-------------------------------------|
|
||||
| **Day** | Bright ambient; high contrast. | DAY-DIM, DAY-NORMAL, DAY-BRIGHT (levels 3–5) by lux bands. |
|
||||
| **Dusk/Dawn** | Transition; reduced luminance. | DUSK mode → level 2. |
|
||||
| **Night** | Low luminance so as **not to affect the mariner’s night vision**. | NIGHT mode → level 1 (minimum). |
|
||||
|
||||
**IHO S-52** (e.g. Edition 6.1.1):
|
||||
- “The ambient lighting on the bridge varies between the extremes of bright sunlight … and night, when the **light emitted by the display has to be low enough that it does not affect the mariner's night vision**.”
|
||||
- Three colour/luminance modes (day, dusk/dawn, night) are defined for ECDIS; the screen-brightness daemon provides a compatible **three-mode luminance framework** (NIGHT / DUSK / DAY-*).
|
||||
|
||||
**IEC 62288** (e.g. IEC 62288:2021):
|
||||
- General requirements, test methods, and required results for the **presentation of navigation-related information** on shipborne navigational displays.
|
||||
- Supports IMO MSC.191(79) and MSC.302(87).
|
||||
- Full luminance limits (e.g. numerical cd/m² for night) are in the purchased standard; the daemon’s “minimum level 1” and “never 0” satisfy the “dimmed but not to extinction” and “night vision” intent.
|
||||
|
||||
**Sources (external):**
|
||||
- IHO S-52 — Specifications for chart content and display aspects of ECDIS (IHO publication).
|
||||
- [IEC 62288:2021](https://webstore.iec.ch/publication/64659) — Maritime navigation and radiocommunication equipment and systems – Presentation of navigation-related information on shipborne navigational displays.
|
||||
|
||||
---
|
||||
|
||||
## 5. Summary table — Regulation vs implementation
|
||||
|
||||
| Regulation | Clause / topic | Daemon behaviour |
|
||||
|------------|----------------|------------------|
|
||||
| **MSC.191(79)** | §8.1.1 Brightness adjustable, dimmable | Levels 1–5 from ambient sensor; configurable lux→level mapping. |
|
||||
| **MSC.191(79)** | §8.1.1 Legible in all ambient conditions | NIGHT / DUSK / DAY-* modes with configurable lux thresholds. |
|
||||
| **MSC.191(79)** | §8.1.1 Not to extinction | Level **0 never used**; minimum level 1. |
|
||||
| **MSC.191(79)** | §8.1.2 Reset to preset | Override file: fixed level 1–5 or `auto`. |
|
||||
| **MSC.302(87)** | Visual alarms high luminance | Buzzer ON → **immediate** level 5. |
|
||||
| **MSC.302(87)** | Dimming not to extinction | Same as above; min level 1. |
|
||||
| **MSC.302(87)** | Night vision not degraded | NIGHT/DUSK low levels; smooth stepping on transitions. |
|
||||
| **MSC.302(87)** | Alert persistence | Cooldown (e.g. 10 s) at level 5 after buzzer stops. |
|
||||
| **IHO S-52 / IEC 62288** | Three modes (day / dusk / night) | NIGHT, DUSK, DAY-DIM, DAY-NORMAL, DAY-BRIGHT. |
|
||||
| **IHO S-52** | Night luminance low for night vision | NIGHT mode → level 1. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration and audit
|
||||
|
||||
- **Config file:** `/etc/screen-brightness.conf` — lux thresholds, levels per mode, cooldown, hysteresis.
|
||||
- **Override (preset):** `/run/screen-brightness/override` — navigator preset (1–5 or `auto`).
|
||||
- **State (read-only):** `/run/screen-brightness/state` — current mode, level, lux, buzzer, override.
|
||||
- **Reload:** `systemctl reload screen-brightness` (no restart).
|
||||
|
||||
For vessel-specific tuning or audits, adjust thresholds and levels in `/etc/screen-brightness.conf` and document the chosen values and the fact that level 0 is never used.
|
||||
|
||||
---
|
||||
|
||||
## 7. References (URLs and documents)
|
||||
|
||||
| Reference | Description | URL or source |
|
||||
|-----------|-------------|----------------|
|
||||
| IMO MSC.191(79) | Performance standards for presentation of navigation-related information on shipborne navigational displays | https://www.imorules.com/MSCRES_191.79.html |
|
||||
| IMO MSC.191(79) §8.1 | Display adjustment (brightness, dimming, preset) | https://www.imorules.com/GUID-6B14EB44-F913-4C8F-8E36-F74CE6E8E3DE.html |
|
||||
| IMO MSC.302(87) | Performance standards for Bridge Alert Management | https://www.imorules.com/MSCRES_302.87.html |
|
||||
| IMO MSC.302(87) Annex | BAM performance standards (Modules A–D) | https://www.imorules.com/MSCRES_302.87_ANN.html |
|
||||
| IHO S-52 | Specifications for chart content and display aspects of ECDIS (e.g. Ed. 6.1.1) | IHO publication; see https://iho.int |
|
||||
| IEC 62288 | Presentation of navigation-related information on shipborne navigational displays | IEC 62288:2021 (and amendments); https://webstore.iec.ch |
|
||||
| reTerminal DM wiki | Hardware interfaces (backlight, buzzer, light sensor) | https://wiki.seeedstudio.com/reterminal-dm/ |
|
||||
|
||||
---
|
||||
|
||||
*This document is for guidance only. For formal compliance, refer to the official IMO, IHO, and IEC publications and your flag state / classification society.*
|
||||
62
emmc-provisioning/docs/SCREEN-BRIGHTNESS-SUMMARY.md
Normal file
62
emmc-provisioning/docs/SCREEN-BRIGHTNESS-SUMMARY.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Screen Brightness Control — Summary
|
||||
|
||||
Short overview of how the reTerminal DM brightness system works.
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **screen-brightness** (daemon) | Reads sensor and override, drives backlight and buzzer; writes `/run/screen-brightness/state`. |
|
||||
| **brightness-api** (HTTP) | Listens on `127.0.0.1:8765`. POST sets override (level 1–5 or auto); GET returns current state. |
|
||||
| **brightness-overlay** (UI) | Small icon in bottom-right of screen; click to expand/collapse panel with buttons 1–5 and Auto. Talks to the API. |
|
||||
|
||||
---
|
||||
|
||||
## Levels and modes
|
||||
|
||||
- **Levels 1–5:** 1 = dimmest, 5 = brightest. Written to `/sys/class/backlight/lcd_backlight/brightness` (0 is never used).
|
||||
- **Auto:** Override file set to `auto` (or missing). Daemon uses ambient light to pick level.
|
||||
- **Manual:** Override file contains `1`–`5`. Daemon keeps that level until user switches back to Auto.
|
||||
|
||||
Override file: `/run/screen-brightness/override`
|
||||
State file (read-only): `/run/screen-brightness/state` (mode, level, lux, override=0|1).
|
||||
|
||||
---
|
||||
|
||||
## On-screen overlay
|
||||
|
||||
- **Icon:** Brightness (sun) icon in the bottom-right corner; transparent when retracted.
|
||||
- **Click icon:** Panel expands with “Brightness”, buttons **1 2 3 4 5**, and **Auto**. Semi-transparent background behind the buttons.
|
||||
- **Active state:** The current level or Auto is highlighted so you can see which is active.
|
||||
- **Click again:** Panel retracts to icon only.
|
||||
|
||||
Overlay uses the local API; if GtkLayerShell is available (Wayland), it stays on top of fullscreen apps.
|
||||
|
||||
---
|
||||
|
||||
## Services and boot
|
||||
|
||||
- **screen-brightness.service** — Starts the daemon. `ExecStartPre` writes `auto` into the override file so every boot starts in Auto.
|
||||
- **brightness-api.service** — Starts the HTTP API (after `screen-brightness`).
|
||||
- **brightness-overlay** — Started from the user session (e.g. autostart) so it appears on the desktop.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Action | How |
|
||||
|--------|-----|
|
||||
| Set level 3 | Overlay: expand → click **3**. Or: `echo 3 > /run/screen-brightness/override` (root). |
|
||||
| Set Auto | Overlay: expand → click **Auto**. Or: `echo auto > /run/screen-brightness/override`. |
|
||||
| Read state | `curl http://127.0.0.1:8765/state` or `cat /run/screen-brightness/state`. |
|
||||
| Config | `/etc/screen-brightness.conf` (optional); `systemctl reload screen-brightness` to apply. |
|
||||
|
||||
For full setup and regulations, see [SCREEN-BRIGHTNESS-MANUAL-SETUP.md](SCREEN-BRIGHTNESS-MANUAL-SETUP.md) and [SCREEN-BRIGHTNESS-REGULATIONS.md](SCREEN-BRIGHTNESS-REGULATIONS.md).
|
||||
@@ -17,8 +17,8 @@ exec >> "$LOG_FILE" 2>&1
|
||||
# Configuration - adjust paths and size for your setup
|
||||
RPIBOOT_DIR="${RPIBOOT_DIR:-/opt/usbboot}"
|
||||
GOLDEN_IMAGE="${GOLDEN_IMAGE:-/var/lib/cm4-provisioning/golden.img}"
|
||||
# Expected eMMC size in bytes. reTerminal DM (CM4) has 32 GB eMMC (~31268536320 bytes).
|
||||
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-$(( 32 * 1024 * 1024 * 1024 ))}"
|
||||
# Expected eMMC size in bytes (optional). If set, used to prefer among multiple new devices; if unset, any new block device after rpiboot is accepted (works for 8/16/32 GB CM4).
|
||||
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-}"
|
||||
LOG_TAG="cm4-flash"
|
||||
STATUS_FILE="${STATUS_FILE:-/var/lib/cm4-provisioning/status.json}"
|
||||
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
||||
@@ -51,7 +51,7 @@ trap 'rm -f "$LOCK_FILE" "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/nul
|
||||
ENABLE_FILE="${ENABLE_FILE:-/etc/cm4-provisioning/enabled}"
|
||||
if [[ -n "$ENABLE_FILE" && ! -f "$ENABLE_FILE" ]]; then
|
||||
log "Skipping: $ENABLE_FILE not present"
|
||||
write_status "idle" "Provisioning disabled (remove /etc/cm4-provisioning/enabled to enable)" "null" 2>/dev/null || true
|
||||
write_status "idle" "Provisioning disabled (touch /etc/cm4-provisioning/enabled to enable)" "null" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -90,8 +90,8 @@ if [[ -z "$RPIBOOT_GADGET" ]]; then
|
||||
write_status "error" "rpiboot gadget missing" "null" "Copy mass-storage-gadget(64) to $RPIBOOT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
# rpiboot requires bootfiles.bin or one of bootcode*.bin in the gadget dir; empty dir causes "No 'bootcode' files found"
|
||||
if [[ ! -f "$RPIBOOT_GADGET/bootfiles.bin" && ! -f "$RPIBOOT_GADGET/bootcode.bin" && ! -f "$RPIBOOT_GADGET/bootcode4.bin" && ! -f "$RPIBOOT_GADGET/bootcode5.bin" ]]; then
|
||||
# rpiboot requires bootfiles.bin, bootcode*.bin, or boot.img in the gadget dir; empty dir causes "No 'bootcode' files found"
|
||||
if [[ ! -f "$RPIBOOT_GADGET/bootfiles.bin" && ! -f "$RPIBOOT_GADGET/bootcode.bin" && ! -f "$RPIBOOT_GADGET/bootcode4.bin" && ! -f "$RPIBOOT_GADGET/bootcode5.bin" && ! -f "$RPIBOOT_GADGET/boot.img" ]]; then
|
||||
log "rpiboot gadget dir has no boot files: $RPIBOOT_GADGET (reinstall usbboot)"
|
||||
write_status "error" "rpiboot gadget empty" "null" "No boot files in $RPIBOOT_GADGET. On the host run: fix-gadget-bootcode-on-host.sh (or from your machine: ssh root@HOST 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh). See docs troubleshooting."
|
||||
exit 1
|
||||
@@ -104,51 +104,86 @@ write_status "rpiboot" "Connecting to CM4 in boot mode…" "0"
|
||||
# Block devices before rpiboot (so we can detect new one after)
|
||||
before_devs=$(lsblk -nd -o NAME 2>/dev/null | sort)
|
||||
|
||||
log "Starting rpiboot to expose CM4 eMMC as mass storage..."
|
||||
# Run rpiboot with 90s timeout so we don't hang if it doesn't exit cleanly when device switches to mass storage
|
||||
log "Starting rpiboot to expose CM4 eMMC as mass storage (gadget: $RPIBOOT_GADGET)..."
|
||||
# Run rpiboot with 180s timeout so device has time to receive bootcode and switch to mass storage; -v for verbose
|
||||
rpiboot_exit=0
|
||||
timeout 90 "$RPIBOOT_BIN" -d "$RPIBOOT_GADGET" || rpiboot_exit=$?
|
||||
timeout 180 "$RPIBOOT_BIN" -v -d "$RPIBOOT_GADGET" || rpiboot_exit=$?
|
||||
# timeout returns 124 if killed by timeout; 0 or other if rpiboot exited on its own
|
||||
if [[ "$rpiboot_exit" -eq 124 ]]; then
|
||||
log "rpiboot timed out after 90s (device may have switched to mass storage)"
|
||||
log "rpiboot timed out after 180s (device may have switched to mass storage)"
|
||||
elif [[ "$rpiboot_exit" -ne 0 ]]; then
|
||||
log "rpiboot exited with code $rpiboot_exit"
|
||||
log "Common causes: (1) No device in USB boot mode — set eMMC disable jumper and use USB slave port. (2) Wrong USB port or cable. (3) Run on host: tail -50 /var/lib/cm4-provisioning/flash.log"
|
||||
write_status "error" "rpiboot failed" "null" "rpiboot failed or no device connected. Check flash.log on host. Try unplug/replug USB."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$(date -Iseconds)] rpiboot finished (exit=$rpiboot_exit); starting device scan"
|
||||
log "rpiboot completed; waiting for block device..."
|
||||
log "rpiboot completed; waiting for new block device (any size — 8/16/32 GB CM4 supported)..."
|
||||
write_status "rpiboot" "rpiboot done, waiting for block device…" "10"
|
||||
|
||||
# rpiboot exits when device switches to mass storage; udev may need several seconds to create /dev/sdX
|
||||
# Poll for new block device for up to 30s (device switch can be slow)
|
||||
# rpiboot exits when device switches to mass storage; udev may need many seconds to create /dev/sdX
|
||||
# Dynamic detection: accept any NEW block device (not present before rpiboot). No fixed eMMC size required.
|
||||
# When rpiboot timed out (124), device may still be switching — wait longer in that case
|
||||
max_wait=60
|
||||
[[ "$rpiboot_exit" -eq 124 ]] && max_wait=90
|
||||
target_dev=""
|
||||
for wait_sec in $(seq 2 2 10) $(seq 12 2 30); do
|
||||
new_devs=""
|
||||
for wait_sec in $(seq 2 2 20) $(seq 22 2 $max_wait); do
|
||||
sleep 2
|
||||
new_devs=""
|
||||
for dev in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
|
||||
[[ -b "$dev" ]] || continue
|
||||
[[ "$dev" =~ [0-9]$ ]] && continue
|
||||
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
|
||||
if [[ -n "$size" ]]; then
|
||||
if (( size >= EMMC_SIZE_BYTES * 95 / 100 && size <= EMMC_SIZE_BYTES * 105 / 100 )); then
|
||||
target_dev=$dev
|
||||
break 2
|
||||
fi
|
||||
if [[ -z "$target_dev" && "$before_devs" != *"${dev#/dev/}"* ]]; then
|
||||
target_dev=$dev
|
||||
fi
|
||||
# Only consider devices that appeared after rpiboot (the CM4 eMMC)
|
||||
if [[ "$before_devs" != *"${dev#/dev/}"* ]]; then
|
||||
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
|
||||
[[ -n "$size" ]] && new_devs="$new_devs $dev:$size"
|
||||
fi
|
||||
done
|
||||
[[ -n "$target_dev" ]] && break
|
||||
new_devs="${new_devs# }"
|
||||
if [[ -n "$new_devs" ]]; then
|
||||
# One new device: use it (dynamic — works for any eMMC size)
|
||||
# Multiple new devices: prefer one matching EMMC_SIZE_BYTES if set, else largest
|
||||
if [[ "$new_devs" != *" "* ]]; then
|
||||
target_dev="${new_devs%%:*}"
|
||||
break
|
||||
else
|
||||
best_dev="" best_size=0 best_delta=999999999999
|
||||
for entry in $new_devs; do
|
||||
dev="${entry%%:*}"
|
||||
size="${entry##*:}"
|
||||
if [[ -z "$EMMC_SIZE_BYTES" || "$EMMC_SIZE_BYTES" -eq 0 ]]; then
|
||||
# No size hint: take largest new device
|
||||
[[ "$size" -gt "$best_size" ]] && { best_dev="$dev"; best_size="$size"; }
|
||||
else
|
||||
delta=$(( size - EMMC_SIZE_BYTES )); [[ "$delta" -lt 0 ]] && delta=$(( -delta ))
|
||||
[[ "$delta" -lt "$best_delta" ]] && { best_dev="$dev"; best_delta="$delta"; }
|
||||
fi
|
||||
done
|
||||
[[ -n "$best_dev" ]] && target_dev="$best_dev" && break
|
||||
fi
|
||||
fi
|
||||
log "Waiting for block device... ${wait_sec}s"
|
||||
write_status "rpiboot" "Waiting for eMMC block device… (${wait_sec}s)" "10"
|
||||
done
|
||||
|
||||
log "Device scan complete. before_devs=[$before_devs] target_dev=[$target_dev]"
|
||||
if [[ -n "$target_dev" ]]; then
|
||||
detected_size=$(blockdev --getsize64 "$target_dev" 2>/dev/null || true)
|
||||
log "Using $target_dev (size=${detected_size:-?} bytes, $(( ${detected_size:-0} / 1024 / 1024 / 1024 )) GB)"
|
||||
fi
|
||||
if [[ -z "$target_dev" ]]; then
|
||||
log "No suitable block device found after rpiboot (expected ~${EMMC_SIZE_BYTES} bytes)"
|
||||
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot"
|
||||
log "No new block device found after rpiboot"
|
||||
log "Current block devices (for debugging):"
|
||||
lsblk -nd -o NAME,SIZE,TYPE 2>/dev/null | while read -r line; do log " $line"; done
|
||||
for d in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
|
||||
[[ -b "$d" ]] || continue
|
||||
[[ "$d" =~ [0-9]$ ]] && continue
|
||||
s=$(blockdev --getsize64 "$d" 2>/dev/null || true)
|
||||
log " $d size=$s ($((${s:-0} / 1024 / 1024 / 1024)) GB)"
|
||||
done
|
||||
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot. Check flash.log on host; unplug/replug and ensure eMMC disable jumper is set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -206,7 +241,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
fi
|
||||
log "Backup complete: $BACKUPS_DIR/$final_name"
|
||||
write_status "done" "Backup complete: $final_name" "100"
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "Backup failed" "null" "dd failed"
|
||||
fi
|
||||
@@ -230,7 +265,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
|
||||
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
|
||||
# Auto-reset status to idle after 90s so dashboard does not stay on this message (dashboard also auto-clears after 60s when open)
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "Flash failed" "null" "dd failed"
|
||||
fi
|
||||
@@ -265,7 +300,7 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
||||
log "EEPROM update written. Remove eMMC disable jumper and power cycle to apply."
|
||||
write_status "done" "EEPROM update written to boot partition. Remove eMMC disable jumper and power cycle the reTerminal to apply." "100"
|
||||
rm -f "$EEPROM_UPD" "$EEPROM_SIG"
|
||||
( sleep 90 && write_status "idle" "Waiting for reTerminal in boot mode or network." "null" ) &
|
||||
( sleep 90 && write_status "idle" "Waiting for Device in USB boot mode." "null" ) &
|
||||
else
|
||||
write_status "error" "EEPROM update failed" "null" "Failed to copy pieeprom.upd/sig"
|
||||
fi
|
||||
|
||||
17
emmc-provisioning/lxc/70-cm4-extra-lan
Normal file
17
emmc-provisioning/lxc/70-cm4-extra-lan
Normal file
@@ -0,0 +1,17 @@
|
||||
# Extra LAN IPs on eth1 and VLAN 40 on eth1.
|
||||
# Primary eth1 address is set by Proxmox/deploy (used by dnsmasq DHCP).
|
||||
# Installed by setup-network-boot-on-lxc.sh; ensure /etc/network/interfaces
|
||||
# includes: source-directory /etc/network/interfaces.d
|
||||
|
||||
# Secondary addresses on eth1 (192.168.30.1, 192.168.127.1)
|
||||
iface eth1 inet static
|
||||
address 192.168.30.1/24
|
||||
iface eth1 inet static
|
||||
address 192.168.127.1/24
|
||||
|
||||
# VLAN 40 on eth1 — 192.168.0.0/24 (gateway 192.168.0.1)
|
||||
# Requires: apt install vlan; host bridge must pass VLAN 40 if using tagged uplink
|
||||
auto eth1.40
|
||||
iface eth1.40 inet static
|
||||
address 192.168.0.1/24
|
||||
vlan-raw-device eth1
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
Config files for the **provisioning LXC** when using **eth1** as a provisioning LAN (DHCP + TFTP for network boot, NAT for internet).
|
||||
|
||||
**LAN subnet:** When you deploy with `DEPLOY_LXC_LAN_SUBNET` (e.g. `10.100.1.1/24`), the deploy script writes `/opt/cm4-provisioning/lan-subnet.conf` inside the LXC with `LAN_GW`, `LAN_CIDR`, and `DHCP_RANGE_START`/`DHCP_RANGE_END`. The setup script and toggle script read this file so dnsmasq, NAT, and PXE options all use the same subnet. If the file is missing, defaults are `10.20.50.1/24` and `10.20.50.100`–`10.20.50.200`.
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| **dnsmasq-network-boot.conf** | dnsmasq: DHCP + TFTP on eth1 only. Copied to `/etc/dnsmasq.d/` by `scripts/setup-network-boot-on-lxc.sh`. |
|
||||
| **nft-nat-lan.conf** | nftables NAT so 10.20.50.0/24 uses eth0 for internet. Applied by the setup script to `/etc/nftables.d/nat-lan.conf`. |
|
||||
| **dnsmasq-network-boot.conf** | Template: dnsmasq DHCP + TFTP on eth1. Setup script writes `/etc/dnsmasq.d/network-boot.conf` using values from `lan-subnet.conf`. |
|
||||
| **nft-nat-lan.conf** | Template: nftables NAT for LAN→WAN (primary + extra subnets + VLAN 40). Setup script writes `/etc/nftables.d/nat-lan.conf`. |
|
||||
| **70-cm4-extra-lan** | Extra LAN IPs on eth1 (192.168.30.1, 192.168.127.1) and VLAN eth1.40 (192.168.0.1/24). Installed to `/etc/network/interfaces.d/` by setup script. |
|
||||
| **toggle-network-boot-dhcp.sh** | Enable/disable PXE (TFTP) on the LXC; copied to `/opt/cm4-provisioning/` by setup script. |
|
||||
|
||||
Setup is done by running (from your machine):
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PXE/network-boot DHCP options (option 66 = next-server, 67 = boot file).
|
||||
# When this file is present, dnsmasq advertises network boot; when removed, devices get DHCP only and boot from local storage.
|
||||
# Toggle with: /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable|disable
|
||||
# Template; toggle script writes the real next-server from /opt/cm4-provisioning/lan-subnet.conf (LAN_GW).
|
||||
dhcp-option=66,10.20.50.1
|
||||
dhcp-option=67,start4cd.elf
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
# dnsmasq: DHCP + TFTP on eth1 only (provisioning LAN).
|
||||
# dnsmasq: DHCP + TFTP + DNS on eth1 only (provisioning LAN).
|
||||
# Install to /etc/dnsmasq.d/network-boot.conf on the LXC.
|
||||
# Restrict to eth1 so we don't interfere with host/other DHCP.
|
||||
# When using setup-network-boot-on-lxc.sh, the actual subnet, DHCP range, and
|
||||
# file.server address come from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh).
|
||||
|
||||
# Listen only on eth1 (provisioning LAN)
|
||||
# Listen only on eth1 (provisioning LAN); setup script adds listen-address for primary + 192.168.30.1, 192.168.127.1, 192.168.0.1 (DNS on all)
|
||||
interface=eth1
|
||||
bind-interfaces
|
||||
|
||||
# DHCP range for devices on eth1 (adjust if you use a different subnet)
|
||||
# DHCP range for devices on eth1 (template; setup script uses lan-subnet.conf)
|
||||
dhcp-range=10.20.50.100,10.20.50.200,12h
|
||||
|
||||
# DNS: file.server -> eth1 IP (LAN_GW) so scripts can use http://file.server/... (setup script writes this)
|
||||
# address=/file.server/10.20.50.1
|
||||
|
||||
# TFTP for Raspberry Pi / CM4 network boot
|
||||
enable-tftp
|
||||
tftp-root=/srv/tftpboot
|
||||
@@ -17,6 +22,3 @@ tftp-root=/srv/tftpboot
|
||||
# Logging (optional; disable in production if too noisy)
|
||||
log-dhcp
|
||||
log-queries
|
||||
|
||||
# Do not use /etc/resolv.conf or act as DNS if you only want DHCP+TFTP
|
||||
port=0
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# nftables: NAT for LAN (eth1) so clients use WAN (eth0) for internet.
|
||||
# nftables: NAT for LAN (eth1 + extra IPs + eth1.40) so clients use WAN (eth0) for internet.
|
||||
# Load with: nft -f /etc/nftables.d/nat-lan.conf
|
||||
# Or use the inline rules in setup-network-boot-on-lxc.sh (no separate file dependency).
|
||||
# When using setup-network-boot-on-lxc.sh, the primary subnet is from lan-subnet.conf (LAN_CIDR).
|
||||
# Extra subnets: 192.168.30.0/24, 192.168.127.0/24 (eth1), 192.168.0.0/24 (eth1.40 VLAN).
|
||||
|
||||
table ip nat {
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
ip saddr 10.20.50.0/24 oifname "eth0" masquerade
|
||||
ip saddr 192.168.30.0/24 oifname "eth0" masquerade
|
||||
ip saddr 192.168.127.0/24 oifname "eth0" masquerade
|
||||
ip saddr 192.168.0.0/24 oifname "eth0" masquerade
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
# When disabled, TFTP is stopped and no boot server is advertised; DHCP still runs.
|
||||
# Usage: toggle-network-boot-dhcp.sh enable | disable | status
|
||||
# Run as root. Install to /opt/cm4-provisioning/toggle-network-boot-dhcp.sh
|
||||
# LAN gateway for TFTP/next-server is read from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh).
|
||||
|
||||
set -e
|
||||
PXE_CONF="/etc/dnsmasq.d/network-boot-pxe.conf"
|
||||
MAIN_CONF="/etc/dnsmasq.d/network-boot.conf"
|
||||
|
||||
LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf"
|
||||
if [[ -f "$LAN_CONF" ]]; then
|
||||
source "$LAN_CONF"
|
||||
else
|
||||
LAN_GW="10.20.50.1"
|
||||
fi
|
||||
|
||||
# Remove enable-tftp / tftp-root from main config if present (legacy; these belong in PXE conf)
|
||||
cleanup_main_conf() {
|
||||
if [ -f "$MAIN_CONF" ] && grep -q 'enable-tftp\|tftp-root' "$MAIN_CONF" 2>/dev/null; then
|
||||
@@ -18,19 +26,19 @@ cleanup_main_conf() {
|
||||
case "${1:-}" in
|
||||
enable)
|
||||
cleanup_main_conf
|
||||
cat > "$PXE_CONF" << 'EOF'
|
||||
cat > "$PXE_CONF" << EOF
|
||||
# PXE/network boot ENABLED - managed by toggle-network-boot-dhcp.sh
|
||||
# TFTP server (only active when network boot is enabled)
|
||||
enable-tftp
|
||||
tftp-root=/srv/tftpboot
|
||||
# BOOTP fields (siaddr = TFTP server, filename = boot file)
|
||||
dhcp-boot=start4cd.elf,,10.20.50.1
|
||||
dhcp-boot=start4cd.elf,,${LAN_GW}
|
||||
# DHCP options 66/67 (some PXE clients prefer these)
|
||||
dhcp-option=66,10.20.50.1
|
||||
dhcp-option=66,${LAN_GW}
|
||||
dhcp-option=67,start4cd.elf
|
||||
EOF
|
||||
systemctl restart dnsmasq 2>/dev/null || service dnsmasq restart 2>/dev/null || true
|
||||
echo "Network boot enabled."
|
||||
echo "Network boot enabled (TFTP next-server: $LAN_GW)."
|
||||
;;
|
||||
disable)
|
||||
cleanup_main_conf
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
[2026-02-18T09:57:49+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095749.log
|
||||
[2026-02-18T09:57:49+02:00] Deploying to root@10.130.60.224 ...
|
||||
[2026-02-18T09:57:49+02:00] [1/4] Cleaning remote staging dir ...
|
||||
[2026-02-18T09:57:50+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
|
||||
[2026-02-18T09:57:50+02:00] [3/4] Running remote install (host + LXC) ...
|
||||
[2026-02-18T08:01:21+00:00] LXC 201 already exists.
|
||||
[2026-02-18T08:01:21+00:00] Host: installing scripts and udev ...
|
||||
[2026-02-18T08:01:22+00:00] Host: env and dirs ...
|
||||
[2026-02-18T08:01:22+00:00] Starting LXC 201 if stopped ...
|
||||
[2026-02-18T08:01:25+00:00] LXC: installing flash scripts ...
|
||||
failed to create file: /opt/cm4-provisioning/: Is a directory
|
||||
[2026-02-18T08:01:45+00:00] LXC: installing dashboard ...
|
||||
failed to create file: /opt/cm4-provisioning/dashboard/: Is a directory
|
||||
failed to create file: /opt/cm4-provisioning/dashboard/templates/: Is a directory
|
||||
[2026-02-18T08:01:59+00:00] Deploy done (remote).
|
||||
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
|
||||
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
|
||||
failed to create file: /opt/cm4-provisioning/dashboard/: Is a directory
|
||||
[2026-02-18T09:58:31+02:00] [4/4] Deploy finished.
|
||||
|
||||
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
|
||||
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
|
||||
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
|
||||
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095749.log
|
||||
@@ -1,20 +0,0 @@
|
||||
[2026-02-18T09:58:59+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095859.log
|
||||
[2026-02-18T09:58:59+02:00] Deploying to root@10.130.60.224 ...
|
||||
[2026-02-18T09:58:59+02:00] [1/4] Cleaning remote staging dir ...
|
||||
[2026-02-18T09:59:00+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
|
||||
[2026-02-18T09:59:00+02:00] [3/4] Running remote install (host + LXC) ...
|
||||
[2026-02-18T08:02:32+00:00] LXC 201 already exists.
|
||||
[2026-02-18T08:02:32+00:00] Host: installing scripts and udev ...
|
||||
[2026-02-18T08:02:32+00:00] Host: env and dirs ...
|
||||
[2026-02-18T08:02:32+00:00] Starting LXC 201 if stopped ...
|
||||
[2026-02-18T08:02:35+00:00] LXC: installing flash scripts ...
|
||||
[2026-02-18T08:02:55+00:00] LXC: installing dashboard ...
|
||||
[2026-02-18T08:03:09+00:00] Deploy done (remote).
|
||||
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
|
||||
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
|
||||
[2026-02-18T09:59:41+02:00] [4/4] Deploy finished.
|
||||
|
||||
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
|
||||
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
|
||||
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
|
||||
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-095859.log
|
||||
@@ -1,20 +0,0 @@
|
||||
[2026-02-18T10:11:19+02:00] Logging to /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-101119.log
|
||||
[2026-02-18T10:11:19+02:00] Deploying to root@10.130.60.224 ...
|
||||
[2026-02-18T10:11:19+02:00] [1/4] Cleaning remote staging dir ...
|
||||
[2026-02-18T10:11:20+02:00] [2/4] Rsync repo to root@10.130.60.224 ...
|
||||
[2026-02-18T10:11:21+02:00] [3/4] Running remote install (host + LXC) ...
|
||||
[2026-02-18T08:14:52+00:00] LXC 201 already exists.
|
||||
[2026-02-18T08:14:52+00:00] Host: installing scripts and udev ...
|
||||
[2026-02-18T08:14:52+00:00] Host: env and dirs ...
|
||||
[2026-02-18T08:14:52+00:00] Starting LXC 201 if stopped ...
|
||||
[2026-02-18T08:14:56+00:00] LXC: installing flash scripts ...
|
||||
[2026-02-18T08:15:16+00:00] LXC: installing dashboard ...
|
||||
[2026-02-18T08:15:30+00:00] Deploy done (remote).
|
||||
Next: Install usbboot on host when online: ssh <host> 'bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh'
|
||||
Next: Enable dashboard in LXC 201: pct exec 201 -- bash -c 'apt-get install -y python3-flask; cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/; systemctl daemon-reload; systemctl enable --now cm4-dashboard'
|
||||
[2026-02-18T10:12:02+02:00] [4/4] Deploy finished.
|
||||
|
||||
Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/).
|
||||
When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
|
||||
Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md).
|
||||
Log written to: /home/nearxos/Projects/reTerminal DM4/chromium-setup/emmc-provisioning/scripts/deploy-20260218-101119.log
|
||||
203
emmc-provisioning/scripts/deploy-screen-brightness-to-device.sh
Executable file
203
emmc-provisioning/scripts/deploy-screen-brightness-to-device.sh
Executable file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy screen-brightness daemon to an existing reTerminal DM.
|
||||
#
|
||||
# Usage:
|
||||
# On device (local files in current directory):
|
||||
# sudo ./deploy-screen-brightness-to-device.sh
|
||||
#
|
||||
# On device (download from file server) — use ONE of these (sudo often strips env):
|
||||
# sudo ./deploy-screen-brightness-to-device.sh "http://file.server:5000/files"
|
||||
# sudo -E BOOTSTRAP_URL="http://file.server:5000/files" ./deploy-screen-brightness-to-device.sh
|
||||
#
|
||||
# From host (deploy via SSH; repo files used from ../cloud-init/fileserver/):
|
||||
# ./deploy-screen-brightness-to-device.sh [user@]hostname
|
||||
#
|
||||
# From host via jump server (bastion):
|
||||
# SSH_JUMP_HOST="user@jump.example.com" ./deploy-screen-brightness-to-device.sh pi@10.0.0.5
|
||||
# Or in ~/.ssh/config set ProxyJump for the device; then no env var needed.
|
||||
#
|
||||
# Does not overwrite existing /etc/screen-brightness.conf (keeps vessel tuning).
|
||||
# See docs/SCREEN-BRIGHTNESS-MANUAL-SETUP.md for full manual steps.
|
||||
|
||||
set -e
|
||||
|
||||
# Unconditional first output so we know the script is running (helps if run via sudo)
|
||||
echo "screen-brightness deploy: starting..."
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" && pwd)"
|
||||
FILESERVER_DIR="$(cd "$SCRIPT_DIR/../cloud-init/fileserver" 2>/dev/null && pwd)"
|
||||
ARG1="${1:-}"
|
||||
|
||||
# If first argument looks like a URL (not user@host), use it as BOOTSTRAP_URL
|
||||
if [[ -n "$ARG1" && "$ARG1" == http* && "$ARG1" != *"@"* ]]; then
|
||||
BOOTSTRAP_URL="${ARG1%/}" # strip trailing slash for clean ${URL}/${name}
|
||||
REMOTE=""
|
||||
else
|
||||
REMOTE="$ARG1"
|
||||
fi
|
||||
|
||||
# Require root for local install
|
||||
if [[ -z "$REMOTE" && "$(id -u)" -ne 0 ]]; then
|
||||
echo "This script must be run as root for local install. Use: sudo $0 ${1:-}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Remote deploy (from host via SSH) ─────────────────────────────────────────
|
||||
# REMOTE is set when first arg is not a URL (e.g. pi@device or Host alias from ~/.ssh/config)
|
||||
if [[ -n "$REMOTE" ]]; then
|
||||
if [[ ! -d "$FILESERVER_DIR" ]]; then
|
||||
echo "ERROR: Fileserver dir not found: $FILESERVER_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Optional: deploy via jump server (e.g. SSH_JUMP_HOST="user@jump.example.com")
|
||||
SSH_OPTS=()
|
||||
SCP_OPTS=()
|
||||
if [[ -n "${SSH_JUMP_HOST:-}" ]]; then
|
||||
SSH_OPTS=(-J "$SSH_JUMP_HOST")
|
||||
SCP_OPTS=(-o "ProxyJump=$SSH_JUMP_HOST")
|
||||
echo "Using jump host: $SSH_JUMP_HOST"
|
||||
fi
|
||||
echo "Deploying screen-brightness + overlay to $REMOTE (from $FILESERVER_DIR) ..."
|
||||
ssh "${SSH_OPTS[@]}" "${REMOTE}" "mkdir -p /tmp/screen-brightness-deploy"
|
||||
scp -q "${SCP_OPTS[@]}" \
|
||||
"$FILESERVER_DIR/screen-brightness.py" \
|
||||
"$FILESERVER_DIR/screen-brightness.service" \
|
||||
"$FILESERVER_DIR/screen-brightness.conf" \
|
||||
"$FILESERVER_DIR/brightness-api.py" \
|
||||
"$FILESERVER_DIR/brightness-api.service" \
|
||||
"$FILESERVER_DIR/brightness-overlay.py" \
|
||||
"$FILESERVER_DIR/brightness-overlay-launch.sh" \
|
||||
"$FILESERVER_DIR/brightness-overlay.desktop" \
|
||||
"${REMOTE}:/tmp/screen-brightness-deploy/"
|
||||
ssh "${SSH_OPTS[@]}" "${REMOTE}" "sudo bash -s" << 'REMOTE_SCRIPT'
|
||||
set -e
|
||||
cd /tmp/screen-brightness-deploy
|
||||
install -m 755 screen-brightness.py /usr/local/bin/screen-brightness.py
|
||||
install -m 644 screen-brightness.service /etc/systemd/system/screen-brightness.service
|
||||
[[ ! -f /etc/screen-brightness.conf ]] && install -m 644 screen-brightness.conf /etc/screen-brightness.conf || true
|
||||
mkdir -p /run/screen-brightness
|
||||
printf '%s\n' 'd /run/screen-brightness 0755 root root -' > /etc/tmpfiles.d/screen-brightness.conf
|
||||
systemctl daemon-reload
|
||||
systemctl enable screen-brightness.service
|
||||
systemctl restart screen-brightness.service
|
||||
echo "Installed and restarted screen-brightness.service"
|
||||
|
||||
# Brightness API (for overlay)
|
||||
install -m 755 brightness-api.py /usr/local/bin/brightness-api.py
|
||||
install -m 644 brightness-api.service /etc/systemd/system/brightness-api.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable brightness-api.service
|
||||
systemctl restart brightness-api.service
|
||||
echo "Installed and restarted brightness-api.service"
|
||||
|
||||
# Overlay (crew manual control) — install for user pi
|
||||
OVERLAY_USER=pi
|
||||
OVERLAY_HOME=/home/$OVERLAY_USER
|
||||
if [[ -f brightness-overlay.py && -f brightness-overlay.desktop ]] && id "$OVERLAY_USER" &>/dev/null; then
|
||||
install -m 755 brightness-overlay.py "$OVERLAY_HOME/brightness-overlay.py"
|
||||
[[ -f brightness-overlay-launch.sh ]] && install -m 755 brightness-overlay-launch.sh "$OVERLAY_HOME/brightness-overlay-launch.sh"
|
||||
mkdir -p "$OVERLAY_HOME/.config/autostart"
|
||||
sed "s|/home/pi|$OVERLAY_HOME|g" brightness-overlay.desktop > "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
|
||||
chown -R "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay.py" "$OVERLAY_HOME/brightness-overlay-launch.sh" "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop" 2>/dev/null || true
|
||||
echo "Installed brightness overlay for $OVERLAY_USER (autostart; starts ~6s after login)"
|
||||
else
|
||||
echo "Skipped overlay (files missing or user $OVERLAY_USER not found)"
|
||||
fi
|
||||
REMOTE_SCRIPT
|
||||
ssh "${SSH_OPTS[@]}" "${REMOTE}" "rm -rf /tmp/screen-brightness-deploy"
|
||||
echo "Done. Check: ssh ${SSH_OPTS[*]} $REMOTE systemctl status screen-brightness.service"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Local deploy (on device) ─────────────────────────────────────────────────
|
||||
if [[ -n "${BOOTSTRAP_URL:-}" ]]; then
|
||||
echo "Downloading from $BOOTSTRAP_URL ..."
|
||||
DOWNLOAD_DIR="/tmp/screen-brightness-deploy"
|
||||
mkdir -p "$DOWNLOAD_DIR"
|
||||
REQUIRED="screen-brightness.py screen-brightness.service screen-brightness.conf"
|
||||
OPTIONAL="brightness-api.py brightness-api.service brightness-overlay.py brightness-overlay-launch.sh brightness-overlay.desktop"
|
||||
for name in $REQUIRED; do
|
||||
url="${BOOTSTRAP_URL}/${name}"
|
||||
if ! curl -fSL "$url" -o "$DOWNLOAD_DIR/$name" 2>/tmp/screen-brightness-curl-err; then
|
||||
echo "ERROR: Failed to download $url" >&2
|
||||
[[ -s /tmp/screen-brightness-curl-err ]] && cat /tmp/screen-brightness-curl-err >&2
|
||||
rm -f /tmp/screen-brightness-curl-err
|
||||
rm -rf "$DOWNLOAD_DIR"
|
||||
exit 1
|
||||
fi
|
||||
rm -f /tmp/screen-brightness-curl-err
|
||||
echo " $name OK"
|
||||
done
|
||||
for name in $OPTIONAL; do
|
||||
url="${BOOTSTRAP_URL}/${name}"
|
||||
if curl -fSL "$url" -o "$DOWNLOAD_DIR/$name" 2>/dev/null; then
|
||||
echo " $name OK"
|
||||
fi
|
||||
done
|
||||
DEPLOY_DIR="$DOWNLOAD_DIR"
|
||||
else
|
||||
if [[ -f "$SCRIPT_DIR/screen-brightness.py" ]]; then
|
||||
DEPLOY_DIR="$SCRIPT_DIR"
|
||||
elif [[ -d "$FILESERVER_DIR" && -f "$FILESERVER_DIR/screen-brightness.py" ]]; then
|
||||
DEPLOY_DIR="$FILESERVER_DIR"
|
||||
else
|
||||
# Assume files are in current directory
|
||||
DEPLOY_DIR="$(pwd)"
|
||||
fi
|
||||
if [[ ! -f "$DEPLOY_DIR/screen-brightness.py" ]]; then
|
||||
echo "ERROR: screen-brightness.py not found." >&2
|
||||
echo " Run from a directory containing screen-brightness.py, .service, .conf" >&2
|
||||
echo " Or set BOOTSTRAP_URL to download from a file server." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Using files from: $DEPLOY_DIR"
|
||||
fi
|
||||
|
||||
install -m 755 "$DEPLOY_DIR/screen-brightness.py" /usr/local/bin/screen-brightness.py
|
||||
install -m 644 "$DEPLOY_DIR/screen-brightness.service" /etc/systemd/system/screen-brightness.service
|
||||
|
||||
if [[ ! -f /etc/screen-brightness.conf ]]; then
|
||||
install -m 644 "$DEPLOY_DIR/screen-brightness.conf" /etc/screen-brightness.conf
|
||||
echo "Installed /etc/screen-brightness.conf"
|
||||
else
|
||||
echo "Keeping existing /etc/screen-brightness.conf"
|
||||
fi
|
||||
|
||||
mkdir -p /run/screen-brightness
|
||||
printf '%s\n' 'd /run/screen-brightness 0755 root root -' > /etc/tmpfiles.d/screen-brightness.conf
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable screen-brightness.service
|
||||
systemctl restart screen-brightness.service
|
||||
echo "Installed and restarted screen-brightness.service"
|
||||
|
||||
# Brightness API (for overlay)
|
||||
if [[ -f "${DEPLOY_DIR:-}/brightness-api.py" ]]; then
|
||||
install -m 755 "$DEPLOY_DIR/brightness-api.py" /usr/local/bin/brightness-api.py
|
||||
if [[ -f "$DEPLOY_DIR/brightness-api.service" ]]; then
|
||||
install -m 644 "$DEPLOY_DIR/brightness-api.service" /etc/systemd/system/brightness-api.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable brightness-api.service
|
||||
systemctl restart brightness-api.service
|
||||
echo "Installed and restarted brightness-api.service"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Overlay (crew manual control)
|
||||
OVERLAY_USER="${SUDO_USER:-pi}"
|
||||
OVERLAY_HOME=$(getent passwd "$OVERLAY_USER" 2>/dev/null | cut -d: -f6)
|
||||
OVERLAY_HOME="${OVERLAY_HOME:-/home/pi}"
|
||||
if [[ -f "${DEPLOY_DIR:-}/brightness-overlay.py" && -f "${DEPLOY_DIR:-}/brightness-overlay.desktop" ]] && [[ -d "$OVERLAY_HOME" ]]; then
|
||||
install -m 755 "$DEPLOY_DIR/brightness-overlay.py" "$OVERLAY_HOME/brightness-overlay.py"
|
||||
[[ -f "${DEPLOY_DIR:-}/brightness-overlay-launch.sh" ]] && install -m 755 "$DEPLOY_DIR/brightness-overlay-launch.sh" "$OVERLAY_HOME/brightness-overlay-launch.sh"
|
||||
mkdir -p "$OVERLAY_HOME/.config/autostart"
|
||||
sed "s|/home/pi|$OVERLAY_HOME|g" "$DEPLOY_DIR/brightness-overlay.desktop" > "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
|
||||
chown "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay.py" "$OVERLAY_HOME/.config/autostart/brightness-overlay.desktop"
|
||||
[[ -f "$OVERLAY_HOME/brightness-overlay-launch.sh" ]] && chown "$OVERLAY_USER:$OVERLAY_USER" "$OVERLAY_HOME/brightness-overlay-launch.sh"
|
||||
echo "Installed brightness overlay for $OVERLAY_USER (autostart; starts ~6s after login)"
|
||||
fi
|
||||
|
||||
[[ "${DEPLOY_DIR:-}" == "/tmp/screen-brightness-deploy" ]] && rm -rf "$DEPLOY_DIR"
|
||||
|
||||
echo ""
|
||||
systemctl status screen-brightness.service --no-pager || true
|
||||
@@ -23,6 +23,13 @@
|
||||
# DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH
|
||||
# DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub)
|
||||
# DEPLOY_LOG=1 — also log to deploy-YYYYMMDD-HHMMSS.log
|
||||
# DEPLOY_LXC_WAN_BRIDGE=vmbr0 — Proxmox bridge for WAN (eth0); default vmbr0
|
||||
# DEPLOY_LXC_WAN_IP=dhcp — WAN address: dhcp (default) or static e.g. 192.168.1.10/24
|
||||
# DEPLOY_LXC_LAN_BRIDGE=vmbr1 — If set, add eth1 as LAN on this bridge (e.g. provisioning / network-boot)
|
||||
# DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 — LXC IP on LAN (gateway); used only if DEPLOY_LXC_LAN_BRIDGE is set; default 10.20.50.1/24
|
||||
# DEPLOY_EMMC_SIZE_GB=32 — optional: eMMC size in GB (used only when multiple new devices appear; default 32). Detection is dynamic — single new device is used regardless of size.
|
||||
#
|
||||
# Legacy: DEPLOY_LXC_NET1="name=eth1,bridge=vmbr1,ip=10.20.50.1/24" still works; overridden by DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET if both are set.
|
||||
#
|
||||
# Requires: ssh key access to root@<host>. For full install (usbboot, PiShrink), host needs internet.
|
||||
|
||||
@@ -144,8 +151,10 @@ rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git'
|
||||
|
||||
log "[4/5] Running remote install (host + LXC) ..."
|
||||
|
||||
# Pass optional LXC SSH vars (base64) and selected storage
|
||||
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE'
|
||||
# Pass optional LXC SSH vars (base64), selected storage, network (WAN/LAN), and eMMC size
|
||||
EMMC_GB="${DEPLOY_EMMC_SIZE_GB:-32}"
|
||||
EMMC_SIZE_BYTES=$(( EMMC_GB * 1024 * 1024 * 1024 ))
|
||||
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}' DEPLOY_LXC_WAN_BRIDGE='${DEPLOY_LXC_WAN_BRIDGE:-}' DEPLOY_LXC_WAN_IP='${DEPLOY_LXC_WAN_IP:-}' DEPLOY_LXC_LAN_BRIDGE='${DEPLOY_LXC_LAN_BRIDGE:-}' DEPLOY_LXC_LAN_SUBNET='${DEPLOY_LXC_LAN_SUBNET:-}' DEPLOY_LXC_NET1='${DEPLOY_LXC_NET1:-}' EMMC_SIZE_BYTES='$EMMC_SIZE_BYTES' EMMC_GB='$EMMC_GB'" bash -s << 'REMOTE'
|
||||
set -e
|
||||
DEPLOY=/tmp/emmc-provisioning-deploy
|
||||
ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}"
|
||||
@@ -171,6 +180,7 @@ for id in $(pct list 2>/dev/null | awk 'NR>1 {print $1}'); do
|
||||
done
|
||||
if [[ -n "$CTID" ]]; then
|
||||
log "Found existing LXC $CTID (hostname: $LXC_HOSTNAME)."
|
||||
pct set "$CTID" -nameserver 8.8.8.8
|
||||
else
|
||||
MAX_ID=$(pct list 2>/dev/null | awk 'NR>1 {print $1}' | sort -n | tail -1)
|
||||
[[ -z "$MAX_ID" ]] && MAX_ID=0
|
||||
@@ -185,18 +195,29 @@ else
|
||||
fi
|
||||
[[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; }
|
||||
TMPL_NAME=$(basename "$DEBIAN12_TMPL")
|
||||
# Optional: add eth1 for network-boot LAN (DHCP+TFTP). Set DEPLOY_LXC_NET1 e.g. "name=eth1,bridge=vmbr1,ip=10.20.50.1/24"
|
||||
# WAN (eth0): bridge and IP from env; default vmbr0 + dhcp
|
||||
WAN_BRIDGE="${DEPLOY_LXC_WAN_BRIDGE:-vmbr0}"
|
||||
WAN_IP="${DEPLOY_LXC_WAN_IP:-dhcp}"
|
||||
# LAN (eth1): optional; use DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET, or legacy DEPLOY_LXC_NET1
|
||||
NET1_OPT=""
|
||||
if [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then
|
||||
if [[ -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]]; then
|
||||
LAN_SUBNET="${DEPLOY_LXC_LAN_SUBNET:-10.20.50.1/24}"
|
||||
NET1_OPT="--net1 name=eth1,bridge=${DEPLOY_LXC_LAN_BRIDGE},ip=${LAN_SUBNET}"
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 LAN bridge=$DEPLOY_LXC_LAN_BRIDGE ip=$LAN_SUBNET"
|
||||
elif [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then
|
||||
NET1_OPT="--net1 $DEPLOY_LXC_NET1"
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 from DEPLOY_LXC_NET1"
|
||||
else
|
||||
log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP (no LAN interface)"
|
||||
fi
|
||||
pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \
|
||||
--hostname "$LXC_HOSTNAME" --memory 1024 --swap 0 --cores 1 \
|
||||
--rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge=vmbr0,ip=dhcp $NET1_OPT \
|
||||
--rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge="$WAN_BRIDGE",ip="$WAN_IP" $NET1_OPT \
|
||||
--unprivileged 0 --features nesting=1 -tag cm4-provisioning
|
||||
pct set "$CTID" -nameserver 8.8.8.8
|
||||
mkdir -p /var/lib/cm4-provisioning
|
||||
pct set "$CTID" -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning
|
||||
log "LXC $CTID created and mount configured."
|
||||
log "LXC $CTID created and mount configured (DNS 8.8.8.8)."
|
||||
fi
|
||||
|
||||
# Optional: bind-mount host directory for backup images (skip if already mounted with same path)
|
||||
@@ -249,11 +270,11 @@ cp "$DEPLOY/host/89-cm4-boot-mode-permissions.rules" /etc/udev/rules.d/ 2>/dev/n
|
||||
cp "$DEPLOY/host/90-cm4-boot-mode.rules" /etc/udev/rules.d/
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
|
||||
log "Host: env and dirs ..."
|
||||
cat > /opt/cm4-provisioning/env << 'ENV'
|
||||
log "Host: env and dirs (EMMC ${EMMC_GB:-32}GB = $EMMC_SIZE_BYTES bytes) ..."
|
||||
cat > /opt/cm4-provisioning/env << ENV
|
||||
GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img
|
||||
RPIBOOT_DIR=/opt/usbboot
|
||||
EMMC_SIZE_BYTES=8589934592
|
||||
EMMC_SIZE_BYTES=${EMMC_SIZE_BYTES:-34359738368}
|
||||
ENV
|
||||
[[ -n "$BACKUPS_HOST_PATH" ]] && echo "BACKUPS_DIR=$BACKUPS_HOST_PATH" >> /opt/cm4-provisioning/env
|
||||
touch /etc/cm4-provisioning/enabled
|
||||
@@ -302,6 +323,25 @@ fi
|
||||
log "Starting LXC $CTID if stopped ..."
|
||||
pct start "$CTID" 2>/dev/null || true
|
||||
|
||||
# --- LXC: write lan-subnet.conf when LAN bridge/subnet is set (so dnsmasq/NAT/toggle use same subnet) ---
|
||||
LAN_SUBNET_FOR_CONF="${DEPLOY_LXC_LAN_SUBNET:-}"
|
||||
[[ -z "$LAN_SUBNET_FOR_CONF" && -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]] && LAN_SUBNET_FOR_CONF="10.20.50.1/24"
|
||||
if [[ -n "$LAN_SUBNET_FOR_CONF" ]]; then
|
||||
if [[ "$LAN_SUBNET_FOR_CONF" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$ ]]; then
|
||||
LAN_GW="${BASH_REMATCH[1]}"
|
||||
PREFIX="${BASH_REMATCH[2]}"
|
||||
BASE_3="${LAN_GW%.*}"
|
||||
LAN_CIDR="${BASE_3}.0/${PREFIX}"
|
||||
DHCP_RANGE_START="${BASE_3}.100"
|
||||
DHCP_RANGE_END="${BASE_3}.200"
|
||||
pct exec "$CTID" -- bash -c "mkdir -p /opt/cm4-provisioning && echo 'LAN_GW=$LAN_GW' > /opt/cm4-provisioning/lan-subnet.conf && echo 'LAN_CIDR=$LAN_CIDR' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_START=$DHCP_RANGE_START' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_END=$DHCP_RANGE_END' >> /opt/cm4-provisioning/lan-subnet.conf"
|
||||
echo "$LAN_GW" > "$DEPLOY/lxc_lan_ip.txt"
|
||||
log "LXC: wrote /opt/cm4-provisioning/lan-subnet.conf (LAN_GW=$LAN_GW); dashboard will be reachable on LAN at http://${LAN_GW}:5000"
|
||||
else
|
||||
log "Warning: DEPLOY_LXC_LAN_SUBNET=$LAN_SUBNET_FOR_CONF not in form A.B.C.D/PREFIX; skipping lan-subnet.conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- LXC: flash scripts (for reference; actual flash runs on host) ---
|
||||
log "LXC: installing flash scripts ..."
|
||||
pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning
|
||||
@@ -309,7 +349,7 @@ pct push "$CTID" "$DEPLOY/host/flash-emmc-on-connect.sh" /opt/cm4-provisioning/f
|
||||
pct exec "$CTID" -- chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
||||
pct push "$CTID" "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/cm4-flash-trigger.sh
|
||||
pct exec "$CTID" -- chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
||||
pct exec "$CTID" -- bash -c 'echo -e "GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img\nRPIBOOT_DIR=/opt/usbboot\nEMMC_SIZE_BYTES=8589934592" > /opt/cm4-provisioning/env'
|
||||
pct exec "$CTID" -- bash -c "echo -e 'GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img\nRPIBOOT_DIR=/opt/usbboot\nEMMC_SIZE_BYTES=$EMMC_SIZE_BYTES' > /opt/cm4-provisioning/env"
|
||||
|
||||
# --- LXC: dashboard (all files) ---
|
||||
log "LXC: installing dashboard ..."
|
||||
@@ -318,6 +358,8 @@ pct push "$CTID" "$DEPLOY/dashboard/app.py" /opt/cm4-provisioning/dashboard/app.
|
||||
pct push "$CTID" "$DEPLOY/dashboard/templates/home.html" /opt/cm4-provisioning/dashboard/templates/home.html
|
||||
pct push "$CTID" "$DEPLOY/dashboard/templates/login.html" /opt/cm4-provisioning/dashboard/templates/login.html
|
||||
pct push "$CTID" "$DEPLOY/dashboard/templates/admin.html" /opt/cm4-provisioning/dashboard/templates/admin.html
|
||||
pct push "$CTID" "$DEPLOY/dashboard/templates/portal_files.html" /opt/cm4-provisioning/dashboard/templates/portal_files.html
|
||||
pct push "$CTID" "$DEPLOY/dashboard/templates/cloudinit_build.html" /opt/cm4-provisioning/dashboard/templates/cloudinit_build.html
|
||||
pct push "$CTID" "$DEPLOY/dashboard/cm4-dashboard.service" /opt/cm4-provisioning/dashboard/cm4-dashboard.service
|
||||
# Dashboard secret for sessions (create once so logins persist across restarts)
|
||||
pct exec "$CTID" -- bash -c '[[ -f /opt/cm4-provisioning/dashboard.env ]] || echo "CM4_DASHBOARD_SECRET_KEY=$(openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | xxd -p)" > /opt/cm4-provisioning/dashboard.env'
|
||||
@@ -363,14 +405,16 @@ log "Deploy done on remote. LXC ID: $CTID"
|
||||
# Heredoc terminator (must be at column 1, no leading space/tab)
|
||||
REMOTE
|
||||
|
||||
# Read LXC IP written by remote (container hostname -I)
|
||||
# Read LXC IP and optional LAN IP written by remote
|
||||
LXC_IP=$(ssh "$PROXMOX" "cat /tmp/emmc-provisioning-deploy/lxc_ip.txt 2>/dev/null" | tr -d '\n\r')
|
||||
LXC_LAN_IP=$(ssh "$PROXMOX" "cat /tmp/emmc-provisioning-deploy/lxc_lan_ip.txt 2>/dev/null" | tr -d '\n\r')
|
||||
|
||||
log "[5/5] Deploy finished."
|
||||
echo ""
|
||||
echo "=== Deploy complete ==="
|
||||
echo "Host and LXC are fully set up: usbboot (rpiboot), PiShrink, dashboard, systemd, udev."
|
||||
[[ -n "$LXC_IP" ]] && echo " LXC IP: $LXC_IP"
|
||||
[[ -n "$LXC_IP" ]] && echo " LXC IP (WAN): $LXC_IP"
|
||||
[[ -n "$LXC_LAN_IP" ]] && echo " LXC IP (LAN): $LXC_LAN_IP"
|
||||
echo ""
|
||||
echo "--- Only remaining step (manual) ---"
|
||||
echo " Add a golden image for Deploy (writing image to device):"
|
||||
@@ -379,7 +423,8 @@ echo " • Or copy your image: scp your-image.img $PROXMOX:/var/lib/cm4-provi
|
||||
echo " Backup (read from device) works without golden.img."
|
||||
echo ""
|
||||
echo "--- You have ---"
|
||||
echo " - Dashboard: http://${LXC_IP:-<LXC-IP>}:5000"
|
||||
echo " - Dashboard (WAN): http://${LXC_IP:-<LXC-IP>}:5000"
|
||||
[[ -n "$LXC_LAN_IP" ]] && echo " - Dashboard (LAN): http://${LXC_LAN_IP}:5000 (use from devices on provisioning LAN)"
|
||||
[[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && [[ -n "$LXC_IP" ]] && echo " - LXC SSH: ssh root@$LXC_IP (password and/or key were set)"
|
||||
[[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && [[ -z "$LXC_IP" ]] && echo " - LXC SSH: ssh root@<LXC-IP> (password and/or key were set)"
|
||||
[[ -n "${CM4_BACKUPS_HOST_PATH:-}" ]] && echo " - Backups on host: $CM4_BACKUPS_HOST_PATH"
|
||||
|
||||
140
emmc-provisioning/scripts/edit-cloudinit-on-image.sh
Executable file
140
emmc-provisioning/scripts/edit-cloudinit-on-image.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mount a Raspberry Pi OS .img or .img.xz, edit cloud-init NoCloud files on the boot
|
||||
# partition, then unmount and (if .img.xz) recompress to a NEW file with current
|
||||
# date/time prefix. Original image is never overwritten. Requires sudo for loop/mount.
|
||||
#
|
||||
# Usage:
|
||||
# ./edit-cloudinit-on-image.sh <path-to-image.img.xz>
|
||||
# ./edit-cloudinit-on-image.sh <path-to-image.img>
|
||||
#
|
||||
# Options:
|
||||
# --no-recompress If image was .img.xz, leave decompressed .img and do not overwrite
|
||||
# --replace-with-repo Copy user-data, meta-data, network-config from repo before editing
|
||||
#
|
||||
# Example:
|
||||
# ./edit-cloudinit-on-image.sh /path/to/gnss-bootstrap-20260223-215010.img.xz
|
||||
#
|
||||
# The original image is preserved; output uses a new timestamp (e.g. gnss-bootstrap-20250305-143022.img.xz).
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CLOUDINIT_SRC="$REPO_ROOT/emmc-provisioning/cloud-init"
|
||||
NO_RECOMPRESS=""
|
||||
REPLACE_WITH_REPO=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-recompress) NO_RECOMPRESS=1; shift ;;
|
||||
--replace-with-repo) REPLACE_WITH_REPO=1; shift ;;
|
||||
-*) echo "Unknown option: $1"; exit 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
IMAGE_IN="$1"
|
||||
[[ -n "$IMAGE_IN" && -f "$IMAGE_IN" ]] || {
|
||||
echo "Usage: $0 [--no-recompress] [--replace-with-repo] <path-to-image.img.xz|.img>"
|
||||
echo "Example: $0 gnss-bootstrap-20260223-215010.img.xz"
|
||||
exit 1
|
||||
}
|
||||
|
||||
IMAGE_IN="$(realpath "$IMAGE_IN")"
|
||||
WORK_DIR=""
|
||||
IMG_FILE=""
|
||||
ORIGINAL_XZ=""
|
||||
cleanup() {
|
||||
if [[ -n "$MNT" && -d "$MNT" ]]; then
|
||||
sudo umount "$MNT" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$LOOP" && -b "$LOOP" ]]; then
|
||||
sudo losetup -d "$LOOP" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
|
||||
if [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then
|
||||
echo "Work dir left at: $WORK_DIR"
|
||||
else
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "$IMAGE_IN" == *.img.xz ]]; then
|
||||
ORIGINAL_XZ="$IMAGE_IN"
|
||||
echo "Decompressing $(basename "$ORIGINAL_XZ") (this may take a minute and needs ~3GB free)…"
|
||||
WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX)
|
||||
IMG_FILE="$WORK_DIR/image.img"
|
||||
xz -T 0 -d -k -f -c "$ORIGINAL_XZ" > "$IMG_FILE"
|
||||
else
|
||||
WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX)
|
||||
IMG_FILE="$IMAGE_IN"
|
||||
fi
|
||||
|
||||
echo "Attaching image and mounting boot partition…"
|
||||
LOOP=$(sudo losetup -f --show -P "$IMG_FILE")
|
||||
# Force kernel to scan partition table so /dev/loopNp1, p2 etc. appear
|
||||
sudo partprobe "$LOOP" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
boot_part="${LOOP}p1"
|
||||
[[ -b "$boot_part" ]] || boot_part="${LOOP}p2"
|
||||
[[ -b "$boot_part" ]] || {
|
||||
echo "Boot partition not found on image. Partitions on image:"
|
||||
ls -la "${LOOP}"p* 2>/dev/null || true
|
||||
sudo fdisk -l "$IMG_FILE" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
MNT="$WORK_DIR/mnt"
|
||||
mkdir -p "$MNT"
|
||||
sudo mount "$boot_part" "$MNT"
|
||||
|
||||
if [[ -n "$REPLACE_WITH_REPO" && -d "$CLOUDINIT_SRC" ]]; then
|
||||
echo "Copying cloud-init files from repo into boot partition…"
|
||||
for f in user-data meta-data network-config; do
|
||||
if [[ -f "$CLOUDINIT_SRC/$f" ]]; then
|
||||
sudo cp "$CLOUDINIT_SRC/$f" "$MNT/$f"
|
||||
elif [[ -f "$CLOUDINIT_SRC/$f.bootstrap" ]]; then
|
||||
sudo cp "$CLOUDINIT_SRC/$f.bootstrap" "$MNT/$f"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Boot partition is mounted at: $MNT"
|
||||
echo "Cloud-init files to edit:"
|
||||
echo " $MNT/user-data"
|
||||
echo " $MNT/meta-data"
|
||||
echo " $MNT/network-config"
|
||||
echo ""
|
||||
EDITOR="${EDITOR:-nano}"
|
||||
echo "Opening editor ($EDITOR). Save and exit when done."
|
||||
read -r -p "Press Enter to open $EDITOR on these files…"
|
||||
sudo "$EDITOR" "$MNT/user-data" "$MNT/meta-data" "$MNT/network-config"
|
||||
|
||||
echo "Unmounting…"
|
||||
sudo umount "$MNT"
|
||||
MNT=""
|
||||
sudo losetup -d "$LOOP"
|
||||
LOOP=""
|
||||
|
||||
if [[ -n "$ORIGINAL_XZ" && -z "$NO_RECOMPRESS" ]]; then
|
||||
ORIG_DIR="$(dirname "$ORIGINAL_XZ")"
|
||||
ORIG_BASE="$(basename "$ORIGINAL_XZ" .img.xz)"
|
||||
if [[ "$ORIG_BASE" =~ ^(.+)-[0-9]{8}-[0-9]{6}$ ]]; then
|
||||
BASE_NAME="${BASH_REMATCH[1]}"
|
||||
else
|
||||
BASE_NAME="$ORIG_BASE"
|
||||
fi
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
NEW_IMG_XZ="$ORIG_DIR/${BASE_NAME}-${TIMESTAMP}.img.xz"
|
||||
echo "Recompressing to $(basename "$NEW_IMG_XZ")…"
|
||||
xz -T 0 -z -f -k "$IMG_FILE"
|
||||
mv -f "${IMG_FILE}.xz" "$NEW_IMG_XZ"
|
||||
echo "Done. New image (original unchanged): $NEW_IMG_XZ"
|
||||
rm -rf "$WORK_DIR"
|
||||
WORK_DIR=""
|
||||
elif [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then
|
||||
echo "Left decompressed image at: $IMG_FILE"
|
||||
echo "Recompress manually: xz -z -k \"$IMG_FILE\""
|
||||
fi
|
||||
@@ -3,6 +3,9 @@
|
||||
# Cause: mass-storage-gadget64 has no real boot files (broken symlinks or Git LFS not pulled).
|
||||
# This script removes broken symlinks and extracts bootcode4.bin from the installed rpiboot binary.
|
||||
#
|
||||
# Does NOT fix: "libusb_bulk_transfer returned -7" / "Failed to write correct length" — that is a USB
|
||||
# transfer/timing issue (try USB 2.0 port, or rpiboot -m 2000). See PROXMOX-LXC-DEPLOYMENT.md.
|
||||
#
|
||||
# On host: bash fix-gadget-bootcode-on-host.sh
|
||||
# From your machine: ssh root@HOST 'bash -s' < emmc-provisioning/scripts/fix-gadget-bootcode-on-host.sh
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup network boot on the provisioning LXC: DHCP + TFTP on eth1, NAT so LAN uses eth0 for internet.
|
||||
# Run inside the LXC (as root), or from your machine: ./setup-network-boot-on-lxc.sh root@10.130.60.141
|
||||
# When run with ssh target, rsyncs lxc/ and runs this script inside the container.
|
||||
# Setup provisioning LXC: DHCP + DNS on eth1, extra LAN IPs, VLAN eth1.40, NAT so LAN uses eth0 for internet.
|
||||
# Network boot (TFTP, PXE, initrd) sections are commented out; uncomment if you need PXE/netboot.
|
||||
# Run inside the LXC (as root), or from your machine: ./setup-network-boot-on-lxc.sh root@10.130.60.141 [SUBNET]
|
||||
# SUBNET optional: A.B.C.D/PREFIX (e.g. 10.100.1.1/24). When run with ssh target, writes lan-subnet.conf on LXC if SUBNET given.
|
||||
# When run with ssh target, rsyncs lxc/ and runs this script inside the container. Subnet is read from /opt/cm4-provisioning/lan-subnet.conf.
|
||||
|
||||
set -e
|
||||
TARGET="${1:-}"
|
||||
SUBNET_ARG="${2:-}"
|
||||
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
# Run remotely: sync lxc/ and script, then execute inside LXC
|
||||
@@ -12,114 +15,190 @@ if [[ -n "$TARGET" ]]; then
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
echo "Syncing lxc config and script to $TARGET ..."
|
||||
rsync -a "$REPO_DIR/lxc/" "$TARGET:/tmp/cm4-network-boot-lxc/" --exclude='.git'
|
||||
if [[ -f "$REPO_DIR/network-boot-initramfs/initrd.img" ]]; then
|
||||
echo "Copying initrd.img to $TARGET ..."
|
||||
scp "$REPO_DIR/network-boot-initramfs/initrd.img" "$TARGET:/tmp/cm4-network-boot-lxc/initrd.img"
|
||||
else
|
||||
echo "Note: network-boot-initramfs/initrd.img not found (run build.sh first); skipping."
|
||||
fi
|
||||
# Network boot: copy initrd.img for TFTP
|
||||
# if [[ -f "$REPO_DIR/network-boot-initramfs/initrd.img" ]]; then
|
||||
# echo "Copying initrd.img to $TARGET ..."
|
||||
# scp "$REPO_DIR/network-boot-initramfs/initrd.img" "$TARGET:/tmp/cm4-network-boot-lxc/initrd.img"
|
||||
# else
|
||||
# echo "Note: network-boot-initramfs/initrd.img not found (run build.sh first); skipping."
|
||||
# fi
|
||||
scp "$SCRIPT_DIR/setup-network-boot-on-lxc.sh" "$TARGET:/tmp/cm4-network-boot-lxc/setup.sh"
|
||||
# If SUBNET_ARG given, write lan-subnet.conf on LXC so inner script uses the set subnet
|
||||
if [[ -n "$SUBNET_ARG" ]]; then
|
||||
if [[ "$SUBNET_ARG" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$ ]]; then
|
||||
LAN_GW="${BASH_REMATCH[1]}"
|
||||
PREFIX="${BASH_REMATCH[2]}"
|
||||
BASE_3="${LAN_GW%.*}"
|
||||
LAN_CIDR="${BASE_3}.0/${PREFIX}"
|
||||
DHCP_RANGE_START="${BASE_3}.100"
|
||||
DHCP_RANGE_END="${BASE_3}.200"
|
||||
ssh "$TARGET" "mkdir -p /opt/cm4-provisioning && echo 'LAN_GW=$LAN_GW' > /opt/cm4-provisioning/lan-subnet.conf && echo 'LAN_CIDR=$LAN_CIDR' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_START=$DHCP_RANGE_START' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_END=$DHCP_RANGE_END' >> /opt/cm4-provisioning/lan-subnet.conf"
|
||||
echo "Wrote lan-subnet.conf on LXC (LAN_GW=$LAN_GW, DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END})."
|
||||
else
|
||||
echo "Warning: SUBNET must be A.B.C.D/PREFIX (e.g. 10.100.1.1/24); ignoring '$SUBNET_ARG'."
|
||||
fi
|
||||
fi
|
||||
ssh "$TARGET" "bash /tmp/cm4-network-boot-lxc/setup.sh"
|
||||
echo "Done."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Running inside the LXC from here ---
|
||||
echo "Configuring network boot (DHCP + TFTP on eth1, NAT via eth0) ..."
|
||||
# LAN subnet: use /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh or passed as SUBNET when running remotely)
|
||||
# Optional first arg when running locally: A.B.C.D/PREFIX to set/write lan-subnet.conf
|
||||
LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf"
|
||||
if [[ "$1" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$ ]]; then
|
||||
LAN_GW="${BASH_REMATCH[1]}"
|
||||
PREFIX="${BASH_REMATCH[2]}"
|
||||
BASE_3="${LAN_GW%.*}"
|
||||
LAN_CIDR="${BASE_3}.0/${PREFIX}"
|
||||
DHCP_RANGE_START="${BASE_3}.100"
|
||||
DHCP_RANGE_END="${BASE_3}.200"
|
||||
mkdir -p /opt/cm4-provisioning
|
||||
echo "LAN_GW=$LAN_GW" > "$LAN_CONF"
|
||||
echo "LAN_CIDR=$LAN_CIDR" >> "$LAN_CONF"
|
||||
echo "DHCP_RANGE_START=$DHCP_RANGE_START" >> "$LAN_CONF"
|
||||
echo "DHCP_RANGE_END=$DHCP_RANGE_END" >> "$LAN_CONF"
|
||||
echo "Using set subnet: $LAN_CIDR (gateway $LAN_GW), DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END}."
|
||||
elif [[ -f "$LAN_CONF" ]]; then
|
||||
source "$LAN_CONF"
|
||||
else
|
||||
LAN_GW="10.20.50.1"
|
||||
LAN_CIDR="10.20.50.0/24"
|
||||
DHCP_RANGE_START="10.20.50.100"
|
||||
DHCP_RANGE_END="10.20.50.200"
|
||||
echo "No lan-subnet.conf and no SUBNET argument; using defaults: $LAN_CIDR."
|
||||
fi
|
||||
echo "Configuring LAN (DHCP + DNS on eth1, NAT via eth0) — LAN $LAN_CIDR (gateway $LAN_GW), DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END} ..."
|
||||
|
||||
# 1) Install dnsmasq
|
||||
# 1) Install dnsmasq and vlan (for eth1.40)
|
||||
if ! command -v dnsmasq >/dev/null 2>&1; then
|
||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq dnsmasq
|
||||
fi
|
||||
if ! command -v vconfig >/dev/null 2>&1; then
|
||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq vlan
|
||||
fi
|
||||
|
||||
# 2) dnsmasq config for eth1 only (DHCP + TFTP); PXE options in network-boot-pxe.conf (toggle with toggle-network-boot-dhcp.sh)
|
||||
# 2) dnsmasq config: DHCP on eth1 only; DNS on all interfaces (no bind-interfaces/listen-address).
|
||||
# PXE/TFTP in network-boot-pxe.conf when needed (toggle-network-boot-dhcp.sh)
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
cat > /etc/dnsmasq.d/network-boot.conf << 'DNSMASQ'
|
||||
# DHCP on eth1 only (provisioning LAN)
|
||||
# TFTP and PXE options are in network-boot-pxe.conf, controlled by toggle-network-boot-dhcp.sh
|
||||
cat > /etc/dnsmasq.d/network-boot.conf << DNSMASQ
|
||||
# DHCP on eth1 only; DNS on all interfaces (eth0, eth1, eth1.40, etc.)
|
||||
interface=eth1
|
||||
bind-interfaces
|
||||
dhcp-range=10.20.50.100,10.20.50.200,12h
|
||||
dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},12h
|
||||
# DNS: file.server resolves to this host (eth1) so scripts can use http://file.server/...
|
||||
address=/file.server/${LAN_GW}
|
||||
# Explicitly send this host as DNS server to DHCP clients (option 6) so they use LXC DNS and resolve file.server
|
||||
dhcp-option=6,${LAN_GW}
|
||||
# Other DNS queries forwarded via LXC's resolv.conf
|
||||
log-dhcp
|
||||
log-queries
|
||||
port=0
|
||||
DNSMASQ
|
||||
mkdir -p /opt/cm4-provisioning
|
||||
if [ -f /tmp/cm4-network-boot-lxc/toggle-network-boot-dhcp.sh ]; then
|
||||
cp /tmp/cm4-network-boot-lxc/toggle-network-boot-dhcp.sh /opt/cm4-provisioning/
|
||||
chmod +x /opt/cm4-provisioning/toggle-network-boot-dhcp.sh
|
||||
/opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable
|
||||
# Network boot: install toggle script and enable PXE/TFTP
|
||||
# if [ -f /tmp/cm4-network-boot-lxc/toggle-network-boot-dhcp.sh ]; then
|
||||
# cp /tmp/cm4-network-boot-lxc/toggle-network-boot-dhcp.sh /opt/cm4-provisioning/
|
||||
# chmod +x /opt/cm4-provisioning/toggle-network-boot-dhcp.sh
|
||||
# /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable
|
||||
# fi
|
||||
|
||||
# 3) Network boot: TFTP root — fetch Raspberry Pi 4 boot files from GitHub if missing
|
||||
# mkdir -p /srv/tftpboot
|
||||
# if [[ ! -f /srv/tftpboot/start4cd.elf ]]; then
|
||||
# echo "Fetching Raspberry Pi firmware boot files from GitHub ..."
|
||||
# if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
|
||||
# apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl
|
||||
# fi
|
||||
# tmpdir=$(mktemp -d)
|
||||
# trap "rm -rf $tmpdir" EXIT
|
||||
# if command -v curl >/dev/null 2>&1; then
|
||||
# curl -sL "https://github.com/raspberrypi/firmware/archive/refs/heads/master.tar.gz" -o "$tmpdir/firmware.tar.gz"
|
||||
# else
|
||||
# wget -q -O "$tmpdir/firmware.tar.gz" "https://github.com/raspberrypi/firmware/archive/refs/heads/master.tar.gz"
|
||||
# fi
|
||||
# tar xzf "$tmpdir/firmware.tar.gz" -C "$tmpdir"
|
||||
# cp -a "$tmpdir/firmware-master/boot/." /srv/tftpboot/
|
||||
# rm -rf "$tmpdir"
|
||||
# echo "Copied RPi boot files to /srv/tftpboot"
|
||||
# else
|
||||
# echo "TFTP root already has boot files (start4cd.elf present), skipping fetch."
|
||||
# fi
|
||||
|
||||
# 3b) Network boot: copy provisioning initrd.img to TFTP root if provided
|
||||
# if [[ -f /tmp/cm4-network-boot-lxc/initrd.img ]]; then
|
||||
# cp /tmp/cm4-network-boot-lxc/initrd.img /srv/tftpboot/initrd.img
|
||||
# echo "Copied initrd.img to /srv/tftpboot"
|
||||
# if [[ -f /srv/tftpboot/config.txt ]] && ! grep -q 'initramfs initrd.img' /srv/tftpboot/config.txt 2>/dev/null; then
|
||||
# echo "" >> /srv/tftpboot/config.txt
|
||||
# echo "# Provisioning initramfs (network-boot-initramfs)" >> /srv/tftpboot/config.txt
|
||||
# echo "initramfs initrd.img followkernel" >> /srv/tftpboot/config.txt
|
||||
# echo "Added initramfs line to config.txt"
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# 4) Extra LAN IPs on eth1 (192.168.30.1, 192.168.127.1) and VLAN eth1.40 (192.168.0.1/24)
|
||||
EXTRA_LAN_CONF="/etc/network/interfaces.d/70-cm4-extra-lan"
|
||||
if [[ -f /tmp/cm4-network-boot-lxc/70-cm4-extra-lan ]]; then
|
||||
mkdir -p /etc/network/interfaces.d
|
||||
cp /tmp/cm4-network-boot-lxc/70-cm4-extra-lan "$EXTRA_LAN_CONF"
|
||||
# Ensure main interfaces includes interfaces.d (Debian)
|
||||
if [[ -f /etc/network/interfaces ]] && ! grep -q 'interfaces.d' /etc/network/interfaces 2>/dev/null; then
|
||||
echo 'source /etc/network/interfaces.d/*' >> /etc/network/interfaces
|
||||
fi
|
||||
# Apply immediately: secondary IPs on eth1
|
||||
ip addr add 192.168.30.1/24 dev eth1 2>/dev/null || true
|
||||
ip addr add 192.168.127.1/24 dev eth1 2>/dev/null || true
|
||||
# Create and bring up eth1.40 (VLAN 40)
|
||||
ip link add link eth1 name eth1.40 type vlan id 40 2>/dev/null || true
|
||||
ip addr add 192.168.0.1/24 dev eth1.40 2>/dev/null || true
|
||||
ip link set eth1.40 up 2>/dev/null || true
|
||||
echo "Extra LAN: eth1 + 192.168.30.1/24, 192.168.127.1/24; eth1.40 192.168.0.1/24 (persisted in $EXTRA_LAN_CONF)"
|
||||
fi
|
||||
|
||||
# 3) TFTP root: fetch Raspberry Pi 4 boot files from GitHub if missing
|
||||
mkdir -p /srv/tftpboot
|
||||
if [[ ! -f /srv/tftpboot/start4cd.elf ]]; then
|
||||
echo "Fetching Raspberry Pi firmware boot files from GitHub ..."
|
||||
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
|
||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl
|
||||
fi
|
||||
tmpdir=$(mktemp -d)
|
||||
trap "rm -rf $tmpdir" EXIT
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sL "https://github.com/raspberrypi/firmware/archive/refs/heads/master.tar.gz" -o "$tmpdir/firmware.tar.gz"
|
||||
else
|
||||
wget -q -O "$tmpdir/firmware.tar.gz" "https://github.com/raspberrypi/firmware/archive/refs/heads/master.tar.gz"
|
||||
fi
|
||||
tar xzf "$tmpdir/firmware.tar.gz" -C "$tmpdir"
|
||||
cp -a "$tmpdir/firmware-master/boot/." /srv/tftpboot/
|
||||
rm -rf "$tmpdir"
|
||||
echo "Copied RPi boot files to /srv/tftpboot"
|
||||
else
|
||||
echo "TFTP root already has boot files (start4cd.elf present), skipping fetch."
|
||||
fi
|
||||
|
||||
# 3b) Copy provisioning initrd.img to TFTP root if provided
|
||||
if [[ -f /tmp/cm4-network-boot-lxc/initrd.img ]]; then
|
||||
cp /tmp/cm4-network-boot-lxc/initrd.img /srv/tftpboot/initrd.img
|
||||
echo "Copied initrd.img to /srv/tftpboot"
|
||||
if [[ -f /srv/tftpboot/config.txt ]] && ! grep -q 'initramfs initrd.img' /srv/tftpboot/config.txt 2>/dev/null; then
|
||||
echo "" >> /srv/tftpboot/config.txt
|
||||
echo "# Provisioning initramfs (network-boot-initramfs)" >> /srv/tftpboot/config.txt
|
||||
echo "initramfs initrd.img followkernel" >> /srv/tftpboot/config.txt
|
||||
echo "Added initramfs line to config.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4) IP forwarding (LAN clients use WAN)
|
||||
# 5) IP forwarding (LAN clients use WAN)
|
||||
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-cm4-network-boot.conf
|
||||
sysctl -p /etc/sysctl.d/99-cm4-network-boot.conf 2>/dev/null || sysctl -w net.ipv4.ip_forward=1
|
||||
|
||||
# 5) NAT: 10.20.50.0/24 -> eth0 (masquerade)
|
||||
# 6) NAT: primary LAN + extra LAN subnets + VLAN 40 -> eth0 (masquerade)
|
||||
# Subnets: primary (from lan-subnet.conf), 192.168.30.0/24, 192.168.127.0/24, 192.168.0.0/24 (eth1.40)
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
mkdir -p /etc/nftables.d
|
||||
cat > /etc/nftables.d/nat-lan.conf << 'NFT'
|
||||
cat > /etc/nftables.d/nat-lan.conf << NFT
|
||||
table ip nat {
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
ip saddr 10.20.50.0/24 oifname "eth0" masquerade
|
||||
ip saddr ${LAN_CIDR} oifname "eth0" masquerade
|
||||
ip saddr 192.168.30.0/24 oifname "eth0" masquerade
|
||||
ip saddr 192.168.127.0/24 oifname "eth0" masquerade
|
||||
ip saddr 192.168.0.0/24 oifname "eth0" masquerade
|
||||
}
|
||||
}
|
||||
NFT
|
||||
if ! nft list table ip nat 2>/dev/null | grep -q postrouting; then
|
||||
nft -f /etc/nftables.d/nat-lan.conf
|
||||
else
|
||||
nft -f /etc/nftables.d/nat-lan.conf
|
||||
fi
|
||||
# Ensure main config includes our drop-in (Debian)
|
||||
if [[ -f /etc/nftables.conf ]] && ! grep -q 'nftables.d/nat-lan' /etc/nftables.conf 2>/dev/null; then
|
||||
echo 'include "/etc/nftables.d/nat-lan.conf"' >> /etc/nftables.conf
|
||||
fi
|
||||
echo "NAT rule added (nftables) and saved to /etc/nftables.d/nat-lan.conf"
|
||||
echo "NAT rules added (nftables): ${LAN_CIDR}, 192.168.30.0/24, 192.168.127.0/24, 192.168.0.0/24 -> eth0"
|
||||
else
|
||||
# Fallback iptables
|
||||
iptables -t nat -C POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE
|
||||
echo "NAT rule added (iptables)."
|
||||
for cidr in "${LAN_CIDR}" 192.168.30.0/24 192.168.127.0/24 192.168.0.0/24; do
|
||||
iptables -t nat -C POSTROUTING -s "$cidr" -o eth0 -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -s "$cidr" -o eth0 -j MASQUERADE
|
||||
done
|
||||
echo "NAT rules added (iptables) for primary LAN and extra subnets."
|
||||
fi
|
||||
|
||||
# 6) Enable and start dnsmasq
|
||||
# 7) Enable and start dnsmasq
|
||||
systemctl enable dnsmasq
|
||||
systemctl restart dnsmasq
|
||||
|
||||
echo "Network boot setup done."
|
||||
echo " - DHCP + TFTP on eth1 (10.20.50.1), range 10.20.50.100-200"
|
||||
echo " - NAT: 10.20.50.0/24 -> eth0 (internet)"
|
||||
echo " - TFTP root: /srv/tftpboot (RPi boot files; initrd.img if provided)"
|
||||
echo "Setup done."
|
||||
echo " - DHCP + DNS on eth1 ($LAN_GW), range ${DHCP_RANGE_START}-${DHCP_RANGE_END}"
|
||||
echo " - NAT: ${LAN_CIDR}, 192.168.30.0/24, 192.168.127.0/24, 192.168.0.0/24 -> eth0 (internet)"
|
||||
echo " - Extra LAN: eth1 also 192.168.30.1, 192.168.127.1; eth1.40 192.168.0.1/24 (VLAN 40)"
|
||||
# echo " - TFTP root: /srv/tftpboot (RPi boot files; initrd.img if provided)" # when network boot enabled
|
||||
|
||||
@@ -12,11 +12,15 @@
|
||||
# ├── first-boot.conf ← cloud-init runcmd downloads this (required)
|
||||
# ├── first-boot.conf.example ← reference
|
||||
# ├── bootstrap.sh ← user-data.bootstrap downloads this (main path)
|
||||
# └── first-boot/ ← FILE_SERVER points here
|
||||
# ├── screen-brightness.py ← manual deploy: .../files/screen-brightness.py
|
||||
# ├── screen-brightness.service
|
||||
# ├── screen-brightness.conf
|
||||
# └── first-boot/ ← FILE_SERVER points here (first-boot + step 14)
|
||||
# ├── steps/
|
||||
# │ ├── 01-hostname.sh … 13-reboot.sh
|
||||
# │ ├── 01-hostname.sh … 14-screen_brightness.sh
|
||||
# ├── start-chromium.sh
|
||||
# ├── splash.png
|
||||
# ├── screen-brightness.py (and .service, .conf)
|
||||
# └── ...
|
||||
#
|
||||
# Usage: ./sync-portal-files-to-lxc.sh [user@lxc_ip]
|
||||
@@ -34,6 +38,10 @@ REMOTE_FIRST_BOOT="${REMOTE_PORTAL}/first-boot"
|
||||
# Files we sync to the portal root (outside first-boot/)
|
||||
PORTAL_ROOT_FILES=(first-boot.sh first-boot.conf first-boot.conf.example bootstrap.sh)
|
||||
|
||||
# Files from fileserver/ that we also copy to portal root so /files/<name> works
|
||||
# (e.g. manual deploy: sudo ./deploy-screen-brightness-to-device.sh "http://HOST:5000/files")
|
||||
FILESERVER_FILES_AT_PORTAL_ROOT=(screen-brightness.py screen-brightness.service screen-brightness.conf)
|
||||
|
||||
# ── Validate local files ────────────────────────────────────────────────
|
||||
if [[ ! -d "$FILESERVER_DIR" ]]; then
|
||||
echo "Error: fileserver dir not found: $FILESERVER_DIR"
|
||||
@@ -67,6 +75,13 @@ for f in "${PORTAL_ROOT_FILES[@]}"; do
|
||||
echo " ✗ $f (not found locally, will skip)"
|
||||
fi
|
||||
done
|
||||
for f in "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}"; do
|
||||
if [[ -f "$FILESERVER_DIR/$f" ]]; then
|
||||
echo " ✓ $f (from fileserver/)"
|
||||
else
|
||||
echo " ✗ $f (not in fileserver/, will skip portal root copy)"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "first-boot/ assets ($REMOTE_FIRST_BOOT/):"
|
||||
CHANGES=$(rsync -avzn --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/" 2>&1 | grep -v '^\(sending\|sent\|total\|$\|building\|\.d\.\.\.\.\.\.\.\.\.\)' | head -40)
|
||||
@@ -98,12 +113,23 @@ echo ""
|
||||
echo "── Syncing fileserver/ → first-boot/ ──────────────────────────"
|
||||
rsync -avz --delete "$FILESERVER_DIR/" "$LXC:$REMOTE_FIRST_BOOT/"
|
||||
|
||||
# ── Copy screen-brightness (and similar) from fileserver to portal root ───
|
||||
# So http://HOST:5000/files/screen-brightness.py works for manual deploy
|
||||
echo ""
|
||||
echo "── Copying fileserver files to portal root ───────────────────"
|
||||
for f in "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}"; do
|
||||
if [[ -f "$FILESERVER_DIR/$f" ]]; then
|
||||
rsync -avz "$FILESERVER_DIR/$f" "$LXC:$REMOTE_PORTAL/"
|
||||
echo " ✓ $f"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Check for extra files in portal root ────────────────────────────────
|
||||
echo ""
|
||||
echo "── Checking for extra files in portal root ────────────────────"
|
||||
|
||||
REMOTE_FILES=$(ssh "$LXC" "find $REMOTE_PORTAL -maxdepth 1 -not -path $REMOTE_PORTAL -printf '%f\n' 2>/dev/null" | sort)
|
||||
EXPECTED_FILES=$(printf '%s\n' "${PORTAL_ROOT_FILES[@]}" "first-boot" | sort -u)
|
||||
EXPECTED_FILES=$(printf '%s\n' "${PORTAL_ROOT_FILES[@]}" "${FILESERVER_FILES_AT_PORTAL_ROOT[@]}" "first-boot" | sort -u)
|
||||
EXTRA_FILES=$(comm -23 <(echo "$REMOTE_FILES") <(echo "$EXPECTED_FILES"))
|
||||
|
||||
if [[ -z "$EXTRA_FILES" ]]; then
|
||||
|
||||
64
emmc-provisioning/scripts/test-usbboot-on-host.sh
Executable file
64
emmc-provisioning/scripts/test-usbboot-on-host.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Manually test rpiboot on the Proxmox host (device in boot mode must be connected).
|
||||
# Usage:
|
||||
# From your machine: ./test-usbboot-on-host.sh [proxmox_host]
|
||||
# On the host: ./test-usbboot-on-host.sh
|
||||
# With timeout (e.g. 60s): TIMEOUT=60 ./test-usbboot-on-host.sh root@100.106.128.36
|
||||
# If you see "Failed to write correct length, returned -7", try USB 2.0 port or add delay:
|
||||
# RPIBOOT_EXTRA_OPTS='-m 2000' ./test-usbboot-on-host.sh root@100.106.128.36
|
||||
#
|
||||
# Replace proxmox_host with your host, e.g. root@100.106.128.36
|
||||
|
||||
set -e
|
||||
HOST="${1:-}"
|
||||
RPIBOOT="${RPIBOOT:-/opt/usbboot/rpiboot}"
|
||||
GADGET="${GADGET:-/opt/usbboot/mass-storage-gadget64}"
|
||||
TIMEOUT="${TIMEOUT:-0}"
|
||||
RPIBOOT_EXTRA_OPTS="${RPIBOOT_EXTRA_OPTS:-}"
|
||||
|
||||
run_on_host() {
|
||||
if [[ -n "$HOST" ]]; then
|
||||
ssh "$HOST" "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Checking usbboot and gadget on ${HOST:-localhost} ==="
|
||||
run_on_host "test -x $RPIBOOT" || { echo "Error: $RPIBOOT not found or not executable"; exit 1; }
|
||||
run_on_host "test -d $GADGET" || { echo "Error: $GADGET not found"; exit 1; }
|
||||
run_on_host "test -f $GADGET/bootcode4.bin || test -f $GADGET/boot.img || test -f $GADGET/bootfiles.bin" || { echo "Error: no boot file in $GADGET"; exit 1; }
|
||||
echo " rpiboot: $RPIBOOT"
|
||||
echo " gadget: $GADGET"
|
||||
echo ""
|
||||
|
||||
echo "=== USB devices (2b8e / 0a5c:2711 = CM4 boot mode) ==="
|
||||
run_on_host "lsusb | grep -E '2b8e|0a5c' || echo ' None. Connect reTerminal with eMMC disable jumper and USB slave port.'"
|
||||
echo ""
|
||||
|
||||
echo "=== Tip: if rpiboot fails with 'Failed to write correct length, returned -7', use a USB 2.0 port, or run: RPIBOOT_EXTRA_OPTS='-m 2000' $0 $* ==="
|
||||
echo ""
|
||||
|
||||
echo "=== Running rpiboot (verbose) — connect device now if not already ==="
|
||||
echo " When the device switches to mass storage, rpiboot will exit and a new /dev/sdX may appear."
|
||||
echo " Use Ctrl+C to stop, or wait for exit."
|
||||
echo ""
|
||||
|
||||
RPIBOOT_CMD="$RPIBOOT -v -d $GADGET $RPIBOOT_EXTRA_OPTS"
|
||||
if [[ -n "$HOST" ]]; then
|
||||
if [[ "$TIMEOUT" -gt 0 ]]; then
|
||||
ssh "$HOST" "timeout $TIMEOUT $RPIBOOT_CMD" || true
|
||||
else
|
||||
ssh -t "$HOST" "$RPIBOOT_CMD" || true
|
||||
fi
|
||||
else
|
||||
if [[ "$TIMEOUT" -gt 0 ]]; then
|
||||
timeout "$TIMEOUT" $RPIBOOT_CMD || true
|
||||
else
|
||||
$RPIBOOT_CMD || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Block devices now (check for new /dev/sdX) ==="
|
||||
run_on_host "lsblk -nd -o NAME,SIZE,TYPE /dev/sd[a-z] 2>/dev/null || true"
|
||||
Reference in New Issue
Block a user