+ ```
+7. Start recording:
+ ```bash
+ sudo systemctl start record.service
+ ```
+
+## Files
+
+```
+clio-pi/
+├── setup.sh # Main setup script (run with sudo)
+├── bin/
+│ ├── record.sh # Recording loop (ffmpeg)
+│ └── gen_recording_status.py # Status page generator
+├── systemd/
+│ └── record.service # Systemd service unit
+└── README.md
+```
+
+## Configuration
+
+Edit `~/bin/record.sh` environment variables:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `AUDIO_DEVICE` | `hw:1` | ALSA device (run `arecord -l` to list) |
+| `SEGMENT_SECONDS` | `7200` | Segment length (2 hours) |
+| `RECORDINGS_DIR` | `~/recordings` | Output directory |
+| `SOURCE_NAME` | `stationary` | Filename prefix |
+
+## Services
+
+| Service | Description |
+|---------|-------------|
+| `record.service` | Recording daemon |
+| `chrony` | NTP time sync |
+| `nginx` | Status page web server |
+
+## Commands
+
+```bash
+# Recording service
+sudo systemctl start record.service
+sudo systemctl stop record.service
+sudo systemctl status record.service
+journalctl -u record.service -f
+
+# Check time sync
+chronyc tracking
+timedatectl
+
+# View status page
+curl http://localhost/
+```
+
+## Syncing Recordings to Server
+
+TODO: Add rsync/sync script to push recordings to server (10.0.0.45)
+
+## Audio Device Setup
+
+List available devices:
+```bash
+arecord -l
+```
+
+Example output:
+```
+card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio]
+```
+
+This would be `hw:1,0` or just `hw:1`.
diff --git a/clio-pi/bin/gen_recording_status.py b/clio-pi/bin/gen_recording_status.py
new file mode 100755
index 0000000..4179a32
--- /dev/null
+++ b/clio-pi/bin/gen_recording_status.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+import argparse
+import datetime as dt
+import html
+import os
+import re
+import shutil
+import statistics
+from pathlib import Path
+from typing import List, Tuple, Optional
+
+FNAME_RE = re.compile(r"^stationary-(\d{4}-\d{2}-\d{2})_(\d{2}-\d{2}-\d{2})\.opus$")
+
+def parse_start(path: Path, tz) -> Optional[dt.datetime]:
+ m = FNAME_RE.match(path.name)
+ if not m:
+ return None
+ date_str, time_str = m.groups()
+ start = dt.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H-%M-%S")
+ return start.replace(tzinfo=tz)
+
+def get_duration_seconds(path: Path) -> Optional[float]:
+ try:
+ from mutagen.oggopus import OggOpus # optional
+ audio = OggOpus(path)
+ return float(getattr(audio.info, "length", 0.0)) or None
+ except Exception:
+ return None
+
+def human_delta(delta: dt.timedelta) -> str:
+ secs = int(abs(delta).total_seconds())
+ d, rem = divmod(secs, 86400)
+ h, rem = divmod(rem, 3600)
+ m, s = divmod(rem, 60)
+ parts = []
+ if d: parts.append(f"{d}d")
+ if h: parts.append(f"{h}h")
+ if m: parts.append(f"{m}m")
+ if not parts: parts.append(f"{s}s")
+ return " ".join(parts)
+
+def merge_intervals(intervals: List[Tuple[dt.datetime, dt.datetime]]) -> List[Tuple[dt.datetime, dt.datetime]]:
+ if not intervals: return []
+ intervals.sort(key=lambda x: x[0])
+ merged = [intervals[0]]
+ for s, e in intervals[1:]:
+ ls, le = merged[-1]
+ if s <= le:
+ merged[-1] = (ls, max(le, e))
+ else:
+ merged.append((s, e))
+ return merged
+
+def day_bounds(d: dt.date, tz):
+ start = dt.datetime(d.year, d.month, d.day, tzinfo=tz)
+ return start, start + dt.timedelta(days=1)
+
+def intersect(a: Tuple[dt.datetime, dt.datetime], b: Tuple[dt.datetime, dt.datetime]) -> Optional[Tuple[dt.datetime, dt.datetime]]:
+ s1, e1 = a; s2, e2 = b
+ s = max(s1, s2); e = min(e1, e2)
+ return (s, e) if s < e else None
+
+def pct(x, total):
+ return 0 if total <= 0 else max(0, min(100, (x/total)*100))
+
+def fmt_ampm(t: dt.datetime) -> str:
+ s = t.strftime("%I:%M%p") # 01:05PM
+ return s.lstrip("0")
+
+def build(args):
+ tz = dt.datetime.now().astimezone().tzinfo
+ rec_dir = Path(args.dir).expanduser().resolve()
+ files = [p for p in rec_dir.glob("*.opus") if p.is_file()]
+ files.sort()
+
+ # Gather metadata
+ items = [] # (path, start_dt, size_bytes, duration_opt)
+ for p in files:
+ s = parse_start(p, tz)
+ if not s:
+ continue
+ dur = get_duration_seconds(p) if args.read_durations else None
+ try:
+ size = p.stat().st_size
+ except Exception:
+ size = 0
+ items.append((p, s, size, dur))
+ items.sort(key=lambda x: x[1])
+
+ starts = [s for _, s, _, _ in items]
+ deltas = [(b - a).total_seconds() for a, b in zip(starts, starts[1:])]
+
+ # Duration policy (defaults: NO inference, assume 2h)
+ if args.infer:
+ sane = [d for d in deltas if args.infer_min_sec <= d <= args.infer_max_sec]
+ inferred_default = (statistics.median(sane) if sane else args.assume_duration_sec or args.infer_default_sec)
+ inferred_default = max(args.infer_min_sec, min(args.infer_max_sec, inferred_default))
+ else:
+ inferred_default = args.assume_duration_sec or args.infer_default_sec
+
+ # Build intervals
+ intervals: List[Tuple[dt.datetime, dt.datetime]] = []
+ for idx, (p, s, _size, dur) in enumerate(items):
+ if dur is None or dur <= 0:
+ dur_use = float(inferred_default)
+ else:
+ dur_use = dur
+ if idx < len(items)-1 and args.guard_gap_sec > 0:
+ to_next = (items[idx+1][1] - s).total_seconds()
+ dur_use = min(dur_use, max(1.0, to_next - args.guard_gap_sec))
+ e = s + dt.timedelta(seconds=max(1.0, dur_use))
+ intervals.append((s, e))
+
+ gen_time = dt.datetime.now(tz)
+ last_start = starts[-1] if starts else None
+
+ # FS usage for recordings partition
+ try:
+ total_b, used_b, free_b = shutil.disk_usage(str(rec_dir))
+ except Exception:
+ total_b = used_b = free_b = 0
+
+ # Window
+ today = gen_time.date()
+ start_date = today - dt.timedelta(days=args.days - 1)
+
+ big_gap = dt.timedelta(minutes=args.gap_yellow_min)
+ green_gap = dt.timedelta(minutes=args.gap_green_min)
+
+ rows = []
+ window_total_bytes = 0
+ for i in range(args.days):
+ d = start_date + dt.timedelta(days=i)
+ d_start, d_end = day_bounds(d, tz)
+
+ # Per-day file count and bytes (count files that START this day)
+ day_files = [(p, s, size) for (p, s, size, _du) in [(p, s, size, du) for (p, s, size, du) in items] if d_start <= s < d_end]
+ day_bytes = sum(size for (_p, _s, size) in day_files)
+ window_total_bytes += day_bytes
+
+ # Intervals overlapping this day
+ day_intervals = []
+ for (s, e) in intervals:
+ inter = intersect((s, e), (d_start, d_end))
+ if inter:
+ day_intervals.append(inter)
+
+ merged = merge_intervals(day_intervals)
+ covered = sum((e - s).total_seconds() for s, e in merged)
+ day_seconds = 86400.0
+ coverage_pct = pct(covered, day_seconds)
+
+ # Max gap includes edges
+ if not merged:
+ max_gap = dt.timedelta(days=1)
+ else:
+ gaps = [merged[0][0] - d_start]
+ gaps += [b[0] - a[1] for a, b in zip(merged, merged[1:])]
+ gaps.append(d_end - merged[-1][1])
+ max_gap = max(gaps) if gaps else dt.timedelta(0)
+
+ # Status
+ count = len(day_files)
+ if count == 0:
+ status = "red"
+ elif max_gap <= green_gap and coverage_pct >= args.green_cover_pct:
+ status = "green"
+ elif max_gap >= big_gap or coverage_pct < args.yellow_cover_pct:
+ status = "yellow"
+ else:
+ status = "yellow"
+
+ # Mini timeline segments
+ segs = []
+ for s, e in merged:
+ left = pct((s - d_start).total_seconds(), day_seconds)
+ width = pct((e - s).total_seconds(), day_seconds)
+ if width > 0:
+ segs.append((left, width))
+
+ # Details list: show ALL spans; AM/PM times
+ details_txt = ", ".join(f"{fmt_ampm(s)}–{fmt_ampm(e)}" for s, e in merged)
+
+ rows.append({
+ "date": d.isoformat(),
+ "count": count,
+ "bytes": day_bytes,
+ "max_gap": max_gap,
+ "coverage_pct": coverage_pct,
+ "status": status,
+ "segments": segs,
+ "details": details_txt or "—",
+ })
+
+ # HTML
+ def hhmm(td: dt.timedelta) -> str:
+ total = int(td.total_seconds())
+ h = total // 3600
+ m = (total % 3600) // 60
+ return f"{h:02d}:{m:02d}"
+
+ def fmt_bytes(n: int) -> str:
+ units = ("B", "KiB", "MiB", "GiB", "TiB", "PiB")
+ x = float(n)
+ for u in units:
+ if x < 1024.0 or u == units[-1]:
+ return f"{int(x)} {u}" if u == "B" else f"{x:.1f} {u}"
+ x /= 1024.0
+
+
+ title = args.title
+ last_str = "—"
+ if last_start:
+ last_str = f"{last_start.strftime('%Y-%m-%d %I:%M:%S %p %Z')} ({human_delta(gen_time - last_start)} ago)"
+
+ # FS usage strings
+ total_str = fmt_bytes(total_b)
+ used_str = fmt_bytes(used_b)
+ free_str = fmt_bytes(free_b)
+ window_bytes_str = fmt_bytes(window_total_bytes)
+
+ html_out = f"""
+
+
+
+
+
+{html.escape(title)}
+
+
+
+ {html.escape(title)}
+ Generated at {gen_time.strftime('%Y-%m-%d %I:%M:%S %p %Z')}
+
+ Last recording started: {html.escape(last_str)}
+ Window: {args.days} days
+ Dir: {html.escape(str(rec_dir))}
+
+
+ Data in window: {window_bytes_str}
+ Filesystem usage (recordings partition): used {used_str} • free {free_str} • total {total_str}
+
+
+
+
+
+ | Date |
+ # Recs |
+ Data |
+ Max gap |
+ Coverage |
+ Spans |
+ Status |
+
+
+
+"""
+ for r in reversed(rows):
+ cls = f"row-{r['status']}"
+ cov = int(round(r["coverage_pct"]))
+ seg_divs = "".join(
+ f''
+ for left, width in r["segments"]
+ )
+ data_str = fmt_bytes(r["bytes"])
+ html_out += f"""
+ | {r['date']} |
+ {r['count']} |
+ {data_str} |
+ {hhmm(r['max_gap'])} |
+
+ {seg_divs}
+ {cov}%
+ |
+ {html.escape(r['details'])} |
+ {r['status']} |
+
+"""
+ html_out += """
+
+ Legend: green = nearly continuous • yellow = gaps or low coverage • red = no recordings
+
+
+"""
+
+ out_path = Path(args.out).resolve()
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ tmp = out_path.with_suffix(out_path.suffix + ".tmp")
+ with open(tmp, "w", encoding="utf-8") as f:
+ f.write(html_out)
+ os.replace(tmp, out_path)
+
+def main():
+ p = argparse.ArgumentParser(description="Generate recordings status HTML")
+ p.add_argument("--dir", default=str(Path.home() / "recordings"),
+ help="Directory containing .opus files (default: ~/recordings)")
+ p.add_argument("--out", default="/var/www/html/index.html",
+ help="HTML output path (default: /var/www/html/index.html)")
+ # Defaults tuned so cron needs no flags:
+ p.add_argument("--days", type=int, default=365, help="How many days to show (default: 365)")
+ p.add_argument("--gap-yellow-min", type=int, default=120, help="Gap (minutes) that marks a day as yellow (default: 120)")
+ p.add_argument("--gap-green-min", type=int, default=15, help="Max gap (minutes) to still be green (default: 15)")
+ p.add_argument("--read-durations", action="store_true",
+ help="Use mutagen to read exact file durations (optional; default off).")
+ p.add_argument("--green-cover-pct", type=float, default=90.0,
+ help="Min coverage%% for green (default: 90)")
+ p.add_argument("--yellow-cover-pct", type=float, default=10.0,
+ help="Coverage%% below which day is yellow (default: 10)")
+ # Duration policy: default is NO inference, assume 2h
+ p.add_argument("--assume-duration-sec", type=int, default=7200,
+ help="Assumed duration (s) when real duration is unavailable. Default: 7200 (2h).")
+ p.add_argument("--infer", action="store_true",
+ help="Enable inferring duration from median start deltas (disabled by default).")
+ p.add_argument("--infer-min-sec", type=int, default=60,
+ help="Ignore deltas smaller than this when inferring (default: 60s)")
+ p.add_argument("--infer-max-sec", type=int, default=6*3600,
+ help="Ignore deltas larger than this when inferring (default: 6h)")
+ p.add_argument("--infer-default-sec", type=int, default=20*60,
+ help="Fallback segment length if nothing else is available (default: 1200s)")
+ p.add_argument("--guard-gap-sec", type=int, default=0,
+ help="Don’t let a segment extend within this many seconds of next start (default: 0)")
+ p.add_argument("--title", default="Portable Recorder Status", help="Page title")
+ args = p.parse_args()
+ build(args)
+
+if __name__ == "__main__":
+ main()
diff --git a/clio-pi/bin/record.sh b/clio-pi/bin/record.sh
new file mode 100755
index 0000000..6e86c46
--- /dev/null
+++ b/clio-pi/bin/record.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -euo pipefail
+
+# Configuration
+AUDIO_DEVICE="${AUDIO_DEVICE:-hw:1}" # ALSA device (run 'arecord -l' to list)
+SEGMENT_SECONDS="${SEGMENT_SECONDS:-7200}" # 2 hours per file
+RECORDINGS_DIR="${RECORDINGS_DIR:-$HOME/recordings}"
+SOURCE_NAME="${SOURCE_NAME:-stationary}" # Prefix for filenames
+
+mkdir -p "${RECORDINGS_DIR}"
+
+echo "Starting recording..."
+echo " Device: ${AUDIO_DEVICE}"
+echo " Segment: ${SEGMENT_SECONDS}s"
+echo " Output: ${RECORDINGS_DIR}/${SOURCE_NAME}-*.opus"
+
+while true; do
+ ffmpeg -f alsa -ac 1 -i "${AUDIO_DEVICE}" \
+ -c:a libopus \
+ -b:a 32k \
+ -f segment \
+ -segment_time "${SEGMENT_SECONDS}" \
+ -strftime 1 \
+ -reset_timestamps 1 \
+ "${RECORDINGS_DIR}/${SOURCE_NAME}-%Y-%m-%d_%H-%M-%S.opus" \
+ 2>&1 | while read line; do echo "[ffmpeg] $line"; done
+
+ echo "Recording stopped, restarting in 5s..."
+ sleep 5
+done
+
diff --git a/clio-pi/setup.sh b/clio-pi/setup.sh
new file mode 100755
index 0000000..3760de2
--- /dev/null
+++ b/clio-pi/setup.sh
@@ -0,0 +1,238 @@
+#!/bin/bash
+set -euo pipefail
+
+# Clio Pi Setup Script
+# Run this after a fresh Raspberry Pi OS install
+#
+# Prerequisites:
+# 1. Flash Raspberry Pi OS Lite to SD card
+# 2. Use Raspberry Pi Imager to pre-configure:
+# - Hostname (e.g., clio-pi)
+# - Username/password
+# - WiFi credentials
+# - Enable SSH
+# 3. Boot Pi and SSH in
+# 4. Copy this repo to /home/pi/clio-pi
+# 5. Run: sudo ./setup.sh
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PI_USER="${PI_USER:-pi}"
+PI_HOME="/home/${PI_USER}"
+
+echo "=== Clio Pi Setup ==="
+echo "User: ${PI_USER}"
+echo "Home: ${PI_HOME}"
+echo ""
+
+# ============================================================
+# 1. System updates
+# ============================================================
+echo ">>> Updating system packages..."
+apt-get update
+apt-get upgrade -y
+
+# ============================================================
+# 2. Install required packages
+# ============================================================
+echo ">>> Installing required packages..."
+apt-get install -y \
+ ffmpeg \
+ python3 \
+ python3-pip \
+ nginx-light \
+ chrony \
+ rsync
+
+# Optional: for reading audio durations
+pip3 install --break-system-packages mutagen || true
+
+# ============================================================
+# 3. Configure NTP/time sync (chrony)
+# ============================================================
+echo ">>> Configuring time sync (chrony)..."
+cat > /etc/chrony/chrony.conf << 'EOF'
+# Use default NTP pools
+pool 2.debian.pool.ntp.org iburst
+
+# Allow the system clock to be stepped in the first 3 updates
+# if its offset is larger than 1 second
+makestep 1.0 3
+
+# Enable kernel synchronization of the real-time clock (RTC)
+rtcsync
+
+# Serve time to the local subnet (optional)
+#allow 10.0.0.0/24
+
+# Log tracking, measurements, statistics
+logdir /var/log/chrony
+EOF
+
+systemctl enable chrony
+systemctl restart chrony
+
+# Set timezone (adjust as needed)
+timedatectl set-timezone America/New_York
+
+echo ">>> Time sync configured. Current time:"
+timedatectl
+
+# ============================================================
+# 4. Configure SSH key-only access
+# ============================================================
+echo ">>> Configuring SSH for key-only access..."
+
+# Ensure .ssh directory exists
+mkdir -p "${PI_HOME}/.ssh"
+chmod 700 "${PI_HOME}/.ssh"
+touch "${PI_HOME}/.ssh/authorized_keys"
+chmod 600 "${PI_HOME}/.ssh/authorized_keys"
+chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/.ssh"
+
+# Harden sshd_config
+SSHD_CONFIG="/etc/ssh/sshd_config"
+
+# Backup original
+cp "${SSHD_CONFIG}" "${SSHD_CONFIG}.bak"
+
+# Disable password authentication
+sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "${SSHD_CONFIG}"
+sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' "${SSHD_CONFIG}"
+sed -i 's/^#*UsePAM.*/UsePAM no/' "${SSHD_CONFIG}"
+sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' "${SSHD_CONFIG}"
+
+# Ensure pubkey auth is enabled
+grep -q "^PubkeyAuthentication" "${SSHD_CONFIG}" || echo "PubkeyAuthentication yes" >> "${SSHD_CONFIG}"
+
+echo ""
+echo "!!! IMPORTANT !!!"
+echo "Before rebooting, add your SSH public key to:"
+echo " ${PI_HOME}/.ssh/authorized_keys"
+echo ""
+echo "From your local machine, run:"
+echo " ssh-copy-id ${PI_USER}@"
+echo ""
+echo "Or manually append your ~/.ssh/id_ed25519.pub (or id_rsa.pub)"
+echo ""
+
+# Don't restart SSH yet - user needs to add their key first
+
+# ============================================================
+# 5. Set up recording directories
+# ============================================================
+echo ">>> Setting up recording directories..."
+mkdir -p "${PI_HOME}/recordings"
+mkdir -p "${PI_HOME}/bin"
+chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/recordings"
+chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/bin"
+
+# ============================================================
+# 6. Install recording scripts
+# ============================================================
+echo ">>> Installing recording scripts..."
+cp "${SCRIPT_DIR}/bin/record.sh" "${PI_HOME}/bin/"
+cp "${SCRIPT_DIR}/bin/gen_recording_status.py" "${PI_HOME}/bin/"
+chmod +x "${PI_HOME}/bin/record.sh"
+chmod +x "${PI_HOME}/bin/gen_recording_status.py"
+chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/bin"
+
+# ============================================================
+# 7. Install systemd service
+# ============================================================
+echo ">>> Installing systemd service..."
+cp "${SCRIPT_DIR}/systemd/record.service" /etc/systemd/system/
+sed -i "s|/home/pi|${PI_HOME}|g" /etc/systemd/system/record.service
+sed -i "s|User=pi|User=${PI_USER}|g" /etc/systemd/system/record.service
+
+systemctl daemon-reload
+systemctl enable record.service
+# Don't start yet - user may need to configure audio device
+
+# ============================================================
+# 8. Set up cron for status page
+# ============================================================
+echo ">>> Setting up cron job for status page..."
+CRON_LINE="*/10 * * * * python3 ${PI_HOME}/bin/gen_recording_status.py --dir ${PI_HOME}/recordings"
+
+# Install cron job for pi user
+(crontab -u "${PI_USER}" -l 2>/dev/null | grep -v gen_recording_status; echo "${CRON_LINE}") | crontab -u "${PI_USER}" -
+
+# ============================================================
+# 9. Configure nginx for status page
+# ============================================================
+echo ">>> Configuring nginx for status page..."
+cat > /etc/nginx/sites-available/default << 'EOF'
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ root /var/www/html;
+ index index.html;
+
+ server_name _;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+}
+EOF
+
+# Ensure www-data can read the status file
+mkdir -p /var/www/html
+chown -R www-data:www-data /var/www/html
+
+# Allow pi user to write to /var/www/html
+usermod -a -G www-data "${PI_USER}" || true
+chmod 775 /var/www/html
+
+systemctl enable nginx
+systemctl restart nginx
+
+# Generate initial status page
+sudo -u "${PI_USER}" python3 "${PI_HOME}/bin/gen_recording_status.py" --dir "${PI_HOME}/recordings" || true
+
+# ============================================================
+# 10. Print audio device info
+# ============================================================
+echo ""
+echo "=== Audio Devices ==="
+arecord -l || echo "(no audio devices found yet)"
+echo ""
+echo "The recording script uses hw:1 by default."
+echo "Edit ${PI_HOME}/bin/record.sh if your device is different."
+
+# ============================================================
+# Summary
+# ============================================================
+echo ""
+echo "============================================================"
+echo "=== Setup Complete ==="
+echo "============================================================"
+echo ""
+echo "Next steps:"
+echo ""
+echo "1. Add your SSH public key:"
+echo " From your computer: ssh-copy-id ${PI_USER}@$(hostname -I | awk '{print $1}')"
+echo ""
+echo "2. Test SSH key login works, then restart SSH:"
+echo " sudo systemctl restart sshd"
+echo ""
+echo "3. Check/configure audio device:"
+echo " arecord -l"
+echo " Edit ~/bin/record.sh if needed (currently uses hw:1)"
+echo ""
+echo "4. Start recording service:"
+echo " sudo systemctl start record.service"
+echo " sudo systemctl status record.service"
+echo ""
+echo "5. View status page:"
+echo " http://$(hostname -I | awk '{print $1}')/"
+echo ""
+echo "Services installed:"
+echo " - chrony (NTP time sync) - RUNNING"
+echo " - nginx (status page) - RUNNING"
+echo " - record.service - ENABLED (not started)"
+echo ""
+echo "Cron job installed:"
+echo " - gen_recording_status.py runs every 10 minutes"
+echo ""
diff --git a/clio-pi/systemd/record.service b/clio-pi/systemd/record.service
new file mode 100644
index 0000000..9e8240a
--- /dev/null
+++ b/clio-pi/systemd/record.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Record
+
+[Service]
+ExecStart=/home/pi/bin/record.sh
+User=pi
+Type=simple
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target