-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·216 lines (197 loc) · 8.1 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·216 lines (197 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#!/usr/bin/env bash
# One-command installer for amp-control.
#
# ./setup.sh
#
# Detects the OS, installs dependencies, helps pick a serial device (or a
# serial-over-IP bridge), writes .env, and optionally installs a background
# service (launchd on macOS, systemd on Linux/Pi).
#
# Non-interactive use (CI / scripted): set any of these env vars to skip prompts
# MODEL=monoprice-6 DEVICE=/dev/ttyUSB0 PORT=8080 AMP_COUNT=1 \
# MDNS_NAME=multizone SERVICE=systemd NONINTERACTIVE=1 ./setup.sh
# SERVICE may be: launchd | systemd | none
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR"
NONINTERACTIVE="${NONINTERACTIVE:-0}"
KNOWN_MODELS="monoprice-6 monoprice-8 monoprice-4 monoprice-39261 dayton-dax66"
say() { printf '\n\033[1m%s\033[0m\n' "$*"; }
info() { printf ' %s\n' "$*"; }
die() { printf '\033[31mError: %s\033[0m\n' "$*" >&2; exit 1; }
ask() { # ask "Prompt" "default" -> echoes answer (default if non-interactive)
local prompt="$1" def="${2:-}" ans=""
if [ "$NONINTERACTIVE" = "1" ]; then echo "$def"; return; fi
if [ -n "$def" ]; then read -r -p "$prompt [$def]: " ans || true; else read -r -p "$prompt: " ans || true; fi
echo "${ans:-$def}"
}
confirm() { # confirm "Question" (default yes) -> returns 0/1
local prompt="$1" ans=""
if [ "$NONINTERACTIVE" = "1" ]; then return 0; fi
read -r -p "$prompt [Y/n]: " ans || true
case "${ans:-y}" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}
# ---- 1. OS + Node -------------------------------------------------------
case "$(uname -s)" in
Darwin) PLATFORM=macos ;;
Linux) PLATFORM=linux ;;
*) die "Unsupported OS: $(uname -s). This installer supports macOS and Linux." ;;
esac
say "Platform: $PLATFORM"
command -v node >/dev/null 2>&1 || die "Node.js 18+ is required but 'node' was not found."
NODE_BIN="$(command -v node)"
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
[ "$NODE_MAJOR" -ge 18 ] || die "Node.js 18+ required (found $(node -v))."
info "Using node $(node -v) at $NODE_BIN"
# ---- 2. Dependencies ----------------------------------------------------
say "Installing dependencies (npm install)"
npm install
# ---- 3. Model -----------------------------------------------------------
say "Amplifier model"
info "Known profiles: $KNOWN_MODELS"
info "(Only monoprice-6 / MPR-6ZHMAUT is hardware-tested; others are unverified.)"
MODEL="$(ask "MODEL" "${MODEL:-monoprice-6}")"
case " $KNOWN_MODELS " in *" $MODEL "*) ;; *) info "Warning: '$MODEL' is not a known profile; server will fall back to monoprice-6." ;; esac
# ---- 4. Serial device ---------------------------------------------------
say "Serial device"
if [ -n "${DEVICE:-}" ]; then
info "Using DEVICE from environment: $DEVICE"
else
info "Scanning for serial ports..."
# Portable array read (macOS ships bash 3.2, which lacks mapfile/readarray).
PORTS=()
while IFS= read -r line; do [ -n "$line" ] && PORTS+=("$line"); done \
< <(node server.js --probe --plain 2>/dev/null || true)
if [ "${#PORTS[@]}" -eq 0 ]; then
info "No local serial ports found."
info "Plug in the USB-serial adapter, or use a serial-over-IP bridge URL."
DEVICE="$(ask "DEVICE (path or socket://host:port)" "socket://192.168.1.50:4001")"
elif [ "${#PORTS[@]}" -eq 1 ]; then
DEVICE="${PORTS[0]}"
info "Found one port: $DEVICE"
confirm "Use it?" || DEVICE="$(ask "DEVICE (path or socket://host:port)" "$DEVICE")"
else
info "Multiple ports found:"
i=0; for p in "${PORTS[@]}"; do info " [$i] $p"; i=$((i+1)); done
if [ "$NONINTERACTIVE" = "1" ]; then
DEVICE="${PORTS[0]}"
else
idx="$(ask "Pick a number (or type a path/socket:// URL)" "0")"
if [[ "$idx" =~ ^[0-9]+$ ]] && [ "$idx" -lt "${#PORTS[@]}" ]; then DEVICE="${PORTS[$idx]}"; else DEVICE="$idx"; fi
fi
fi
fi
info "DEVICE = $DEVICE"
# ---- 5. Other settings + write .env ------------------------------------
say "Settings"
PORT="$(ask "HTTP PORT" "${PORT:-8080}")"
AMP_COUNT="$(ask "Number of linked amps" "${AMP_COUNT:-1}")"
MDNS_NAME="$(ask "mDNS name (blank to disable)" "${MDNS_NAME:-multizone}")"
ENV_FILE="$DIR/.env"
if [ -f "$ENV_FILE" ]; then
BACKUP="$ENV_FILE.bak.$(date +%Y%m%d%H%M%S)"
cp "$ENV_FILE" "$BACKUP"
info "Existing .env backed up to $(basename "$BACKUP")"
fi
# Values are double-quoted so a space or '#' in a path/URL can't corrupt parsing
# (dotenv strips the surrounding quotes).
cat > "$ENV_FILE" <<EOF
# Generated by setup.sh on $(date)
MODEL="$MODEL"
DEVICE="$DEVICE"
PORT="$PORT"
AMP_COUNT="$AMP_COUNT"
MDNS_NAME="$MDNS_NAME"
EOF
info "Wrote $ENV_FILE"
# ---- 6. Service ---------------------------------------------------------
say "Background service"
DEFAULT_SERVICE="none"; [ "$PLATFORM" = macos ] && DEFAULT_SERVICE="launchd"; [ "$PLATFORM" = linux ] && DEFAULT_SERVICE="systemd"
SERVICE="${SERVICE:-$DEFAULT_SERVICE}"
if [ "$NONINTERACTIVE" != "1" ]; then
info "Install a service so the app starts on boot and restarts on crash?"
SERVICE="$(ask "Service type (launchd/systemd/none)" "$SERVICE")"
fi
install_launchd() {
local label="${SERVICE_LABEL:-com.ampcontrol.server}"
local plist="$HOME/Library/LaunchAgents/$label.plist"
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>$label</string>
<key>ProgramArguments</key>
<array>
<string>$NODE_BIN</string>
<string>$DIR/server.js</string>
</array>
<key>WorkingDirectory</key><string>$DIR</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>$DIR/ampcontrol.log</string>
<key>StandardErrorPath</key><string>$DIR/ampcontrol.log</string>
</dict>
</plist>
EOF
info "Wrote $plist"
launchctl unload "$plist" >/dev/null 2>&1 || true
launchctl load -w "$plist"
info "Loaded service '$label'. Logs: $DIR/ampcontrol.log"
# launchd appends to the log forever; add a newsyslog rule so it can't grow
# unbounded (systemd logs to journald, which already rotates — see below).
local nsconf="/etc/newsyslog.d/ampcontrol.conf"
if confirm "Install a newsyslog rule to rotate the log (keep 5 × ~1MB, needs sudo)?"; then
if printf '%s\t%s:staff\t644\t5\t1024\t*\tJ\n' "$DIR/ampcontrol.log" "$USER" | sudo tee "$nsconf" >/dev/null; then
info "Wrote $nsconf (rotates at ~1MB, keeps 5 compressed)."
else
info "Could not write $nsconf — skipped (log rotation not installed)."
fi
else
info "Skipped log rotation."
fi
}
install_systemd() {
local unit_name="${SERVICE_LABEL:-ampcontrol}"
local unit="$DIR/$unit_name.service"
cat > "$unit" <<EOF
[Unit]
Description=amp-control multizone amplifier web control
After=network.target
[Service]
Type=simple
WorkingDirectory=$DIR
ExecStart=$NODE_BIN $DIR/server.js
Restart=always
RestartSec=5
User=$USER
# Serial access usually needs the dialout group; harmless if unused (socket://).
SupplementaryGroups=dialout
[Install]
WantedBy=multi-user.target
EOF
info "Wrote $unit"
if confirm "Install it as a system service now (requires sudo)?"; then
sudo cp "$unit" "/etc/systemd/system/$unit_name.service"
sudo systemctl daemon-reload
sudo systemctl enable --now "$unit_name.service"
info "Enabled and started '$unit_name.service'. Logs: sudo journalctl -u $unit_name -f"
else
info "Skipped. To install later:"
info " sudo cp $unit /etc/systemd/system/$unit_name.service"
info " sudo systemctl daemon-reload && sudo systemctl enable --now $unit_name.service"
fi
}
case "$SERVICE" in
launchd) [ "$PLATFORM" = macos ] || die "launchd is macOS-only."; install_launchd ;;
systemd) [ "$PLATFORM" = linux ] || die "systemd path is Linux-only."; install_systemd ;;
none) info "No service installed. Start manually with: npm start" ;;
*) die "Unknown SERVICE '$SERVICE' (use launchd, systemd, or none)." ;;
esac
# ---- Done ---------------------------------------------------------------
say "Done."
URL="http://localhost:$PORT"
[ -n "$MDNS_NAME" ] && URL="$URL / http://$MDNS_NAME.local:$PORT"
info "Open: $URL"
[ "$SERVICE" = none ] && info "Run now with: npm start"