Add DX Cluster source selection with DX Spider telnet support

pull/27/head
accius 5 days ago
parent a81e24d5f6
commit 4e8134e44d

@ -668,25 +668,31 @@
return { data, loading };
};
const useDXCluster = () => {
const useDXCluster = (source = 'auto') => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [activeSource, setActiveSource] = useState('');
useEffect(() => {
const fetchDX = async () => {
try {
// Use our proxy endpoint (works when running via server.js)
const response = await fetch('/api/dxcluster/spots');
// Use our proxy endpoint with source parameter
const response = await fetch(`/api/dxcluster/spots?source=${source}`);
if (response.ok) {
const spots = await response.json();
if (spots && spots.length > 0) {
// Track the active source from the first spot
if (spots[0].source) {
setActiveSource(spots[0].source);
}
setData(spots.slice(0, 15).map(s => ({
freq: s.freq || (s.frequency ? (parseFloat(s.frequency) / 1000).toFixed(3) : '0.000'),
call: s.call || s.dx_call || 'UNKNOWN',
comment: s.comment || s.info || '',
time: s.time || new Date().toISOString().substr(11, 5) + 'z',
spotter: s.spotter || ''
spotter: s.spotter || '',
source: s.source || ''
})));
} else {
setData([{
@ -696,6 +702,7 @@
time: '--:--z',
spotter: ''
}]);
setActiveSource('');
}
} else {
setData([{
@ -705,6 +712,7 @@
time: '--:--z',
spotter: ''
}]);
setActiveSource('');
}
} catch (err) {
console.error('DX Cluster error:', err);
@ -715,6 +723,7 @@
time: '--:--z',
spotter: ''
}]);
setActiveSource('');
} finally {
setLoading(false);
}
@ -722,9 +731,9 @@
fetchDX();
const interval = setInterval(fetchDX, DEFAULT_CONFIG.refreshIntervals.dxCluster);
return () => clearInterval(interval);
}, []);
}, [source]);
return { data, loading };
return { data, loading, activeSource };
};
// ============================================
@ -1883,12 +1892,13 @@
);
};
const DXClusterPanel = ({ spots, loading }) => (
const DXClusterPanel = ({ spots, loading, activeSource }) => (
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '20px', backdropFilter: 'blur(10px)', maxHeight: '280px', overflow: 'hidden' }}>
<div style={{ fontSize: '12px', fontWeight: '600', color: 'var(--accent-cyan)', letterSpacing: '2px', marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span>🌐 DX CLUSTER</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{loading && <div className="loading-spinner" />}
{activeSource && <span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>{activeSource}</span>}
<span style={{ fontSize: '12px', color: 'var(--accent-green)' }}>● LIVE</span>
</div>
</div>
@ -2193,7 +2203,10 @@
<div style={{ ...panelStyle, gridRow: '2', gridColumn: '3', display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto' }}>
{/* DX Cluster */}
<div>
<div style={{ ...labelStyle, marginBottom: '8px' }}>🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '12px' }}>● LIVE</span></div>
<div style={{ ...labelStyle, marginBottom: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '12px' }}>● LIVE</span></span>
{dxCluster.activeSource && <span style={{ fontSize: '9px', color: 'var(--text-muted)', fontWeight: '400' }}>{dxCluster.activeSource}</span>}
</div>
<div style={{ maxHeight: '180px', overflowY: 'auto' }}>
{dxCluster.data.slice(0, 8).map((s, i) => (
<div key={i} style={{ padding: '4px 0', borderBottom: '1px solid rgba(255,255,255,0.05)', fontSize: '12px' }}>
@ -2316,6 +2329,7 @@
const [useGeolocation, setUseGeolocation] = useState(false);
const [theme, setTheme] = useState(config.theme || 'dark');
const [layout, setLayout] = useState(config.layout || 'modern');
const [dxClusterSource, setDxClusterSource] = useState(config.dxClusterSource || 'auto');
// Calculate grid square from lat/lon
useEffect(() => {
@ -2372,7 +2386,8 @@
callsign: callsign.toUpperCase().trim(),
location: { lat: latNum, lon: lonNum },
theme: theme,
layout: layout
layout: layout,
dxClusterSource: dxClusterSource
};
onSave(newConfig);
@ -2566,6 +2581,34 @@
</p>
</div>
{/* DX Cluster Source Selection */}
<div style={{ marginBottom: '24px' }}>
<label style={labelStyle}>DX Cluster Source</label>
<select
value={dxClusterSource}
onChange={(e) => setDxClusterSource(e.target.value)}
style={{
width: '100%', padding: '12px 16px', background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)', borderRadius: '6px',
color: 'var(--accent-cyan)', fontFamily: 'JetBrains Mono, monospace',
fontSize: '14px', outline: 'none', cursor: 'pointer'
}}
>
<option value="auto">Auto (Best Available)</option>
<option value="hamqth">HamQTH</option>
<option value="dxheat">DXHeat</option>
<option value="dxsummit">DX Summit</option>
<option value="dxspider">DX Spider (G6NHU)</option>
</select>
<p style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '8px' }}>
{dxClusterSource === 'auto' && '→ Automatically selects the best available source'}
{dxClusterSource === 'hamqth' && '→ HamQTH.com DX Cluster feed'}
{dxClusterSource === 'dxheat' && '→ DXHeat.com real-time cluster'}
{dxClusterSource === 'dxsummit' && '→ DXSummit.fi cluster (may be slow)'}
{dxClusterSource === 'dxspider' && '→ G6NHU-2 DX Spider node (dxspider.co.uk)'}
</p>
</div>
{/* Buttons */}
<div style={{ display: 'flex', gap: '12px' }}>
<button
@ -2637,7 +2680,7 @@
const spaceWeather = useSpaceWeather();
const bandConditions = useBandConditions(spaceWeather.data);
const potaSpots = usePOTASpots();
const dxCluster = useDXCluster();
const dxCluster = useDXCluster(config.dxClusterSource || 'auto');
const contests = useContests();
const propagation = usePropagation(config.location, dxLocation);
const mySpots = useMySpots(config.callsign);
@ -2884,8 +2927,9 @@
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }}>
{/* DX Cluster - Compact */}
<div className="panel" style={{ padding: '10px', flex: '1 1 auto', overflow: 'hidden', minHeight: 0 }}>
<div style={{ fontSize: '12px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '6px' }}>
🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '12px' }}>● LIVE</span>
<div style={{ fontSize: '12px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '12px' }}>● LIVE</span></span>
{dxCluster.activeSource && <span style={{ fontSize: '9px', color: 'var(--text-muted)', fontWeight: '400' }}>{dxCluster.activeSource}</span>}
</div>
<div style={{ overflow: 'auto', maxHeight: 'calc(100% - 20px)' }}>
{dxCluster.data.slice(0, 8).map((s, i) => (

@ -15,6 +15,7 @@ const express = require('express');
const cors = require('cors');
const path = require('path');
const fetch = require('node-fetch');
const net = require('net');
const app = express();
const PORT = process.env.PORT || 3000;
@ -115,163 +116,307 @@ app.get('/api/hamqsl/conditions', async (req, res) => {
}
});
// DX Cluster proxy - fetches from multiple sources
// DX Cluster proxy - fetches from selectable sources
// Query param: ?source=hamqth|dxheat|dxsummit|dxspider|auto (default: auto)
// Cache for DX Spider telnet spots (to avoid too many connections)
let dxSpiderCache = { spots: [], timestamp: 0 };
const DXSPIDER_CACHE_TTL = 60000; // 60 seconds cache
app.get('/api/dxcluster/spots', async (req, res) => {
// Source 1: HamQTH (uses ^ delimiter!)
try {
const source = (req.query.source || 'auto').toLowerCase();
// Helper function for HamQTH
async function fetchHamQTH() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch('https://www.hamqth.com/dxc_csv.php', {
headers: { 'User-Agent': 'OpenHamClock/3.3' },
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const text = await response.text();
const lines = text.trim().split('\n').filter(line => line.trim() && !line.startsWith('#'));
try {
const response = await fetch('https://www.hamqth.com/dxc_csv.php', {
headers: { 'User-Agent': 'OpenHamClock/3.4' },
signal: controller.signal
});
clearTimeout(timeout);
if (lines.length > 0) {
const spots = [];
for (const line of lines.slice(0, 25)) {
// HamQTH format uses ^ delimiter:
// spotter^freq^dx_call^comment^time date^^^continent^band^country^id
// Example: F5PAC^7022.0^KP5/NP3VI^Up 2^0610 2026-01-30^^^NA^40M^Desecheo Island^43
const parts = line.split('^');
if (parts.length >= 5) {
const spotter = parts[0] || '';
const freqKhz = parts[1] || '';
const dxCall = parts[2] || '';
const comment = parts[3] || '';
const timeDate = parts[4] || ''; // "0610 2026-01-30"
const band = parts[9] || '';
if (response.ok) {
const text = await response.text();
const lines = text.trim().split('\n').filter(line => line.trim() && !line.startsWith('#'));
if (lines.length > 0) {
const spots = [];
for (const line of lines.slice(0, 25)) {
const parts = line.split('^');
// Parse frequency (already in kHz, convert to MHz)
const freqNum = parseFloat(freqKhz);
if (!isNaN(freqNum) && freqNum > 0 && dxCall) {
// Convert kHz to MHz for display (7022.0 -> 7.022)
const freqMhz = (freqNum / 1000).toFixed(3);
if (parts.length >= 5) {
const spotter = parts[0] || '';
const freqKhz = parts[1] || '';
const dxCall = parts[2] || '';
const comment = parts[3] || '';
const timeDate = parts[4] || '';
const band = parts[9] || '';
// Extract time from "0610 2026-01-30" -> "06:10z"
let time = '';
if (timeDate && timeDate.length >= 4) {
const timeStr = timeDate.substring(0, 4);
time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
const freqNum = parseFloat(freqKhz);
if (!isNaN(freqNum) && freqNum > 0 && dxCall) {
const freqMhz = (freqNum / 1000).toFixed(3);
let time = '';
if (timeDate && timeDate.length >= 4) {
const timeStr = timeDate.substring(0, 4);
time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
}
spots.push({
freq: freqMhz,
call: dxCall,
comment: comment + (band ? ' ' + band : ''),
time: time,
spotter: spotter,
source: 'HamQTH'
});
}
spots.push({
freq: freqMhz,
call: dxCall,
comment: comment + (band ? ' ' + band : ''),
time: time,
spotter: spotter
});
}
}
if (spots.length > 0) {
console.log('[DX Cluster] HamQTH:', spots.length, 'spots');
return spots;
}
}
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] HamQTH error:', error.message);
}
}
return null;
}
// Helper function for DXHeat
async function fetchDXHeat() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch('https://dxheat.com/dxc/data.php', {
headers: {
'User-Agent': 'OpenHamClock/3.4',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const text = await response.text();
const data = JSON.parse(text);
const spots = data.spots || data;
console.log('[DX Cluster] HamQTH:', spots.length, 'spots');
if (spots.length > 0) {
return res.json(spots);
if (Array.isArray(spots) && spots.length > 0) {
const mapped = spots.slice(0, 25).map(spot => ({
freq: spot.f || spot.frequency || '0.000',
call: spot.c || spot.dx || spot.callsign || 'UNKNOWN',
comment: spot.i || spot.info || '',
time: spot.t ? String(spot.t).substring(11, 16) + 'z' : '',
spotter: spot.s || spot.spotter || '',
source: 'DXHeat'
}));
console.log('[DX Cluster] DXHeat:', mapped.length, 'spots');
return mapped;
}
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DXHeat error:', error.message);
}
}
} catch (error) {
if (error.name === 'AbortError') {
// Timeout - normal if API is slow, try next source
} else {
console.error('[DX Cluster] HamQTH error:', error.message);
}
return null;
}
// Source 2: DX Summit
try {
// Helper function for DX Summit
async function fetchDXSummit() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch('https://www.dxsummit.fi/api/v1/spots?limit=25', {
headers: {
'User-Agent': 'OpenHamClock/3.3 (Amateur Radio Dashboard)',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const text = await response.text();
try {
const response = await fetch('https://www.dxsummit.fi/api/v1/spots?limit=25', {
headers: {
'User-Agent': 'OpenHamClock/3.4 (Amateur Radio Dashboard)',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
try {
if (response.ok) {
const text = await response.text();
const data = JSON.parse(text);
if (Array.isArray(data) && data.length > 0) {
const spots = data.slice(0, 20).map(spot => ({
const spots = data.slice(0, 25).map(spot => ({
freq: spot.frequency ? String(spot.frequency) : '0.000',
call: spot.dx_call || spot.dxcall || spot.callsign || 'UNKNOWN',
comment: spot.info || spot.comment || '',
time: spot.time ? String(spot.time).substring(0, 5) + 'z' : '',
spotter: spot.spotter || spot.de || ''
spotter: spot.spotter || spot.de || '',
source: 'DX Summit'
}));
console.log('[DX Cluster] DX Summit:', spots.length, 'spots');
return res.json(spots);
return spots;
}
} catch (parseErr) {
// Parse error, try next source
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DX Summit error:', error.message);
}
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DX Summit error:', error.message);
}
return null;
}
// Source 3: DXHeat (backup)
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch('https://dxheat.com/dxc/data.php', {
headers: {
'User-Agent': 'OpenHamClock/3.3',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
// Helper function for DX Spider (G6NHU-2) via telnet
async function fetchDXSpider() {
// Check cache first
if (Date.now() - dxSpiderCache.timestamp < DXSPIDER_CACHE_TTL && dxSpiderCache.spots.length > 0) {
console.log('[DX Cluster] DX Spider: returning', dxSpiderCache.spots.length, 'cached spots');
return dxSpiderCache.spots;
}
if (response.ok) {
const text = await response.text();
return new Promise((resolve) => {
const spots = [];
let buffer = '';
let gotSpots = false;
let loginSent = false;
let commandSent = false;
try {
const data = JSON.parse(text);
const spots = data.spots || data;
const client = new net.Socket();
client.setTimeout(12000);
client.connect(7300, 'dxspider.co.uk', () => {
console.log('[DX Cluster] DX Spider: connected');
});
client.on('data', (data) => {
buffer += data.toString();
if (Array.isArray(spots) && spots.length > 0) {
const mapped = spots.slice(0, 20).map(spot => ({
freq: spot.f || spot.frequency || '0.000',
call: spot.c || spot.dx || spot.callsign || 'UNKNOWN',
comment: spot.i || spot.info || '',
time: spot.t ? String(spot.t).substring(11, 16) + 'z' : '',
spotter: spot.s || spot.spotter || ''
}));
console.log('[DX Cluster] DXHeat:', mapped.length, 'spots');
return res.json(mapped);
// Wait for login prompt
if (!loginSent && (buffer.includes('login:') || buffer.includes('Please enter your call'))) {
loginSent = true;
// Send a guest login (GUEST or anonymous callsign)
client.write('GUEST\r\n');
return;
}
} catch (parseErr) {
// Parse error
}
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DXHeat error:', error.message);
}
// Wait for prompt after login, then send command
if (loginSent && !commandSent && (buffer.includes('Hello') || buffer.includes('de ') || buffer.includes('>'))) {
commandSent = true;
// Request last 25 spots
setTimeout(() => {
client.write('sh/dx 25\r\n');
}, 500);
return;
}
// Parse DX spots from the output
// Format: DX de W3LPL: 14195.0 TI5/AA8HH FT8 -09 dB 1234Z
const lines = buffer.split('\n');
for (const line of lines) {
if (line.includes('DX de ')) {
const match = line.match(/DX de ([A-Z0-9\/\-]+):\s+(\d+\.?\d*)\s+([A-Z0-9\/\-]+)\s+(.+?)\s+(\d{4})Z/i);
if (match) {
const spotter = match[1].replace(':', '');
const freqKhz = parseFloat(match[2]);
const dxCall = match[3];
const comment = match[4].trim();
const timeStr = match[5];
if (!isNaN(freqKhz) && freqKhz > 0 && dxCall) {
const freqMhz = (freqKhz / 1000).toFixed(3);
const time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
// Avoid duplicates
if (!spots.find(s => s.call === dxCall && s.freq === freqMhz)) {
spots.push({
freq: freqMhz,
call: dxCall,
comment: comment,
time: time,
spotter: spotter,
source: 'DX Spider'
});
gotSpots = true;
}
}
}
}
}
// If we have enough spots or see end marker, close connection
if (spots.length >= 20 || buffer.includes('G6NHU-2 de GUEST')) {
client.write('bye\r\n');
setTimeout(() => client.destroy(), 500);
}
});
client.on('timeout', () => {
console.log('[DX Cluster] DX Spider: timeout');
client.destroy();
});
client.on('error', (err) => {
console.error('[DX Cluster] DX Spider error:', err.message);
client.destroy();
});
client.on('close', () => {
if (spots.length > 0) {
console.log('[DX Cluster] DX Spider:', spots.length, 'spots');
dxSpiderCache = { spots: spots, timestamp: Date.now() };
resolve(spots);
} else {
resolve(null);
}
});
// Fallback timeout
setTimeout(() => {
if (!gotSpots) {
client.destroy();
}
}, 15000);
});
}
// Fetch based on selected source
let spots = null;
if (source === 'hamqth') {
spots = await fetchHamQTH();
} else if (source === 'dxheat') {
spots = await fetchDXHeat();
} else if (source === 'dxsummit') {
spots = await fetchDXSummit();
} else if (source === 'dxspider') {
spots = await fetchDXSpider();
} else {
// Auto mode - try sources in order
spots = await fetchHamQTH();
if (!spots) spots = await fetchDXHeat();
if (!spots) spots = await fetchDXSummit();
}
res.json(spots || []);
});
// All sources failed or timed out
res.json([]);
// Get available DX cluster sources
app.get('/api/dxcluster/sources', (req, res) => {
res.json([
{ id: 'auto', name: 'Auto (Best Available)', description: 'Automatically selects the best available source' },
{ id: 'hamqth', name: 'HamQTH', description: 'HamQTH.com DX Cluster feed' },
{ id: 'dxheat', name: 'DXHeat', description: 'DXHeat.com real-time cluster' },
{ id: 'dxsummit', name: 'DX Summit', description: 'DXSummit.fi cluster (may be slow)' },
{ id: 'dxspider', name: 'DX Spider (G6NHU)', description: 'G6NHU-2 DX Spider node via telnet (dxspider.co.uk)' }
]);
});
// ============================================

@ -1,194 +0,0 @@
#!/bin/bash
#
# OpenHamClock Raspberry Pi Setup Script
# Configures Pi for kiosk mode operation
#
# Usage: chmod +x setup-pi.sh && ./setup-pi.sh
#
set -e
echo "========================================"
echo " OpenHamClock Raspberry Pi Setup"
echo "========================================"
echo ""
# Check if running on Raspberry Pi
if [ ! -f /proc/device-tree/model ]; then
echo "Warning: This doesn't appear to be a Raspberry Pi."
echo "Continuing anyway..."
fi
# Get the current user
CURRENT_USER=$(whoami)
HOME_DIR=$(eval echo ~$CURRENT_USER)
OPENHAMCLOCK_DIR="$HOME_DIR/openhamclock"
echo "Installing for user: $CURRENT_USER"
echo "Install directory: $OPENHAMCLOCK_DIR"
echo ""
# Update system
echo ">>> Updating system packages..."
sudo apt-get update -qq
# Install required packages
echo ">>> Installing required packages..."
sudo apt-get install -y -qq \
chromium \
unclutter \
xdotool \
x11-xserver-utils
# Create OpenHamClock directory if it doesn't exist
echo ">>> Setting up OpenHamClock directory..."
mkdir -p "$OPENHAMCLOCK_DIR"
# Copy index.html if it exists in the current directory
if [ -f "index.html" ]; then
cp index.html "$OPENHAMCLOCK_DIR/"
echo ">>> Copied index.html to $OPENHAMCLOCK_DIR"
fi
# Create the autostart directory
echo ">>> Configuring autostart..."
mkdir -p "$HOME_DIR/.config/autostart"
# Create autostart entry for OpenHamClock
cat > "$HOME_DIR/.config/autostart/openhamclock.desktop" << EOF
[Desktop Entry]
Type=Application
Name=OpenHamClock
Comment=Amateur Radio Dashboard
Exec=/bin/bash $OPENHAMCLOCK_DIR/start-kiosk.sh
Terminal=false
Hidden=false
X-GNOME-Autostart-enabled=true
EOF
# Create kiosk start script
echo ">>> Creating kiosk start script..."
cat > "$OPENHAMCLOCK_DIR/start-kiosk.sh" << 'EOF'
#!/bin/bash
#
# OpenHamClock Kiosk Mode Launcher
#
# Wait for desktop to be ready
sleep 5
# Disable screen blanking and power management
xset s off
xset -dpms
xset s noblank
# Hide the mouse cursor after 3 seconds of inactivity
unclutter -idle 3 -root &
# Kill any existing Chromium processes
pkill -f chromium || true
sleep 2
# Start Chromium in kiosk mode
chromium \
--kiosk \
--noerrdialogs \
--disable-infobars \
--disable-session-crashed-bubble \
--disable-restore-session-state \
--disable-features=TranslateUI \
--check-for-update-interval=31536000 \
--disable-component-update \
--overscroll-history-navigation=0 \
--incognito \
"file://$HOME/openhamclock/index.html"
EOF
chmod +x "$OPENHAMCLOCK_DIR/start-kiosk.sh"
# Create a stop script
cat > "$OPENHAMCLOCK_DIR/stop-kiosk.sh" << 'EOF'
#!/bin/bash
# Stop OpenHamClock kiosk mode
pkill -f chromium-browser
pkill -f unclutter
echo "OpenHamClock stopped."
EOF
chmod +x "$OPENHAMCLOCK_DIR/stop-kiosk.sh"
# Create a restart script
cat > "$OPENHAMCLOCK_DIR/restart-kiosk.sh" << 'EOF'
#!/bin/bash
# Restart OpenHamClock
$HOME/openhamclock/stop-kiosk.sh
sleep 2
$HOME/openhamclock/start-kiosk.sh &
EOF
chmod +x "$OPENHAMCLOCK_DIR/restart-kiosk.sh"
# Create systemd service for headless operation (optional)
echo ">>> Creating systemd service (for headless operation)..."
sudo tee /etc/systemd/system/openhamclock.service > /dev/null << EOF
[Unit]
Description=OpenHamClock Kiosk
After=graphical-session.target
[Service]
Type=simple
User=$CURRENT_USER
Environment=DISPLAY=:0
ExecStart=/bin/bash $OPENHAMCLOCK_DIR/start-kiosk.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=graphical-session.target
EOF
# Disable screen blanking in config.txt
echo ">>> Configuring boot options..."
if ! grep -q "consoleblank=0" /boot/cmdline.txt 2>/dev/null; then
sudo sed -i '$ s/$/ consoleblank=0/' /boot/cmdline.txt 2>/dev/null || true
fi
# Configure GPU memory for better graphics (optional)
if ! grep -q "gpu_mem=" /boot/config.txt 2>/dev/null; then
echo "gpu_mem=128" | sudo tee -a /boot/config.txt > /dev/null 2>/dev/null || true
fi
echo ""
echo "========================================"
echo " Setup Complete!"
echo "========================================"
echo ""
echo "OpenHamClock has been installed to: $OPENHAMCLOCK_DIR"
echo ""
echo "Files created:"
echo " - $OPENHAMCLOCK_DIR/index.html (main application)"
echo " - $OPENHAMCLOCK_DIR/start-kiosk.sh (start in kiosk mode)"
echo " - $OPENHAMCLOCK_DIR/stop-kiosk.sh (stop kiosk)"
echo " - $OPENHAMCLOCK_DIR/restart-kiosk.sh (restart kiosk)"
echo ""
echo "Auto-start:"
echo " OpenHamClock will automatically start on next boot."
echo ""
echo "Manual commands:"
echo " Start: ~/openhamclock/start-kiosk.sh"
echo " Stop: ~/openhamclock/stop-kiosk.sh"
echo " Restart: ~/openhamclock/restart-kiosk.sh"
echo ""
echo "To disable auto-start:"
echo " rm ~/.config/autostart/openhamclock.desktop"
echo ""
echo "Reboot recommended to apply all changes."
echo ""
echo "73 de OpenHamClock!"
echo ""
read -p "Would you like to reboot now? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
sudo reboot
fi
Loading…
Cancel
Save

Powered by TurnKey Linux.