diff --git a/README.md b/README.md index 043699d..b2f7004 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,35 @@ -# pistarlog2ws - -pistarlog2ws is a small Python script that provides a websocket output to a Pi-Star to be used by L.A.S.S.I.E. +# pistarlog2ws -Installation: +**pistarlog2ws** is a small Python script that provides a websocket output of the radio log of a Pi-Star to be used by L.A.S.S.I.E.. + +## Installation + +### Prerequisites + +You need [Pi-Star](https://www.pistar.uk/) installed and configured to the frequency of your repeater output on a Raspberry Pi with MMDVM HAT. You also need to enable SSH in the configuration of the Pi-Star. + +*Pi-Star's disk is read-only by default, so you have to enable read-write with ```rpi-rw```.* + +Clone the repository into the pi-star home and run the install script: + +```shell +rpi-rw +git clone https://git.furcom.org/dingo/pistarlog2ws +cd pistarlog2ws +./install.sh +``` + +This should install and enable the websocket service and make the necessary adjustments to the firewall. + +## Test + +You can test the installation from another machine in the network using ```nc``` or ```wscat```. + +```shell +nc -zv 8765 +``` + +```shell +wscat -c ws://:8765 --no-color +``` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..e8c06b2 --- /dev/null +++ b/install.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Define paths +SCRIPT_DIR="$HOME/bin" +SCRIPT_PATH="$SCRIPT_DIR/pistarlog2ws.py" +SERVICE_PATH="/etc/systemd/system/pistarlog2ws.service" + +# Ensure required packages are installed +echo "Installing dependencies..." +sudo apt update +sudo apt install -y python3 python3-pip python3-aiofiles python3-websockets python3-tz + +# Create the script directory if it does not exist +mkdir -p "$SCRIPT_DIR" + +# Copy the provided pistarlog2ws.py script +if [[ -f "pistarlog2ws.py" ]]; then + echo "Copying pistarlog2ws.py to $SCRIPT_PATH..." + cp pistarlog2ws.py "$SCRIPT_PATH" +else + echo "Error: pistarlog2ws.py not found in the current directory!" + exit 1 +fi + +# Create the systemd service file +echo "Creating systemd service..." +sudo tee "$SERVICE_PATH" > /dev/null << EOF +[Unit] +Description=Log to WebSocket Server +After=network.target + +[Service] +ExecStart=/usr/bin/python3 $SCRIPT_PATH +Restart=always +User=$USER +WorkingDirectory=$SCRIPT_DIR +StandardOutput=append:/var/log/pistarlog2ws.log +StandardError=append:/var/log/pistarlog2ws.log +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +# Open netfilter port +echo "Opening port (Netfilter)..." +sudo tee /root/ipv4.fw > /dev/null << EOF +iptables -I INPUT 1 -p tcp --dport 8765 -j ACCEPT +EOF + +sudo pistar-firewall + +# Reload systemd to recognize new service +echo "Enabling and starting the service..." +sudo systemctl daemon-reload +sudo systemctl enable pistarlog2ws.service +sudo systemctl start pistarlog2ws.service + +echo "Installation complete. Use the following commands to manage the service:" +echo " sudo systemctl start pistarlog2ws.service # Start the service" +echo " sudo systemctl stop pistarlog2ws.service # Stop the service" +echo " sudo systemctl restart pistarlog2ws.service # Restart the service" +echo " sudo systemctl status pistarlog2ws.service # Check the status" \ No newline at end of file diff --git a/pistarlog2ws.py b/pistarlog2ws.py new file mode 100644 index 0000000..efff9c9 --- /dev/null +++ b/pistarlog2ws.py @@ -0,0 +1,77 @@ +"""Imports""" +import asyncio +import os +import traceback +from datetime import datetime +import websockets +import aiofiles + +LOG_DIR = "/var/log/pi-star" +LOG_PREFIX = "MMDVM-" +LOG_EXT = ".log" +KEYWORDS = ["DMR"] # Add more keywords here + + +def get_logfile_path(): + """Return the correct logfile path based on UTC.""" + date_str = datetime.utcnow().strftime("%Y-%m-%d") + return os.path.join(LOG_DIR, f"{LOG_PREFIX}{date_str}{LOG_EXT}") + + +async def tail_log(websocket, path): + """Read the latest log file and stream lines with specified keywords.""" + last_checked_file = get_logfile_path() + + print(f"Client connected: {websocket.remote_address}") + + # Wait for the file to exist + while not os.path.exists(last_checked_file): + print(f"Waiting for log file: {last_checked_file}") + await asyncio.sleep(2) + last_checked_file = get_logfile_path() + + file = await aiofiles.open(last_checked_file, "r") + await file.seek(0, 2) # Move to the end of the file + + try: + while True: + current_logfile = get_logfile_path() + if current_logfile != last_checked_file: + print(f"Switching to new log file: {current_logfile}") + await file.close() # Explicitly close previous file + last_checked_file = current_logfile + + # Wait until the new log file is ready + while not os.path.exists(last_checked_file): + print(f"Waiting for new log file: {last_checked_file}") + await asyncio.sleep(2) + + file = await aiofiles.open(last_checked_file, "r") + await file.seek(0, 2) # Go to end of new file + + line = await file.readline() + if line: + print(f"READ LINE: {line.strip()}") + if any(keyword in line for keyword in KEYWORDS): + print(f"SENDING: {line.strip()}") + await websocket.send(line.strip()) + else: + await asyncio.sleep(0.5) # Prevent busy-waiting + except websockets.exceptions.ConnectionClosed: + print(f"Client {websocket.remote_address} disconnected.") + except Exception as error: + print(f"Unexpected error: {error}") + traceback.print_exc() + finally: + await file.close() # Ensure file closure + + +async def main(): + """Start the WebSocket server.""" + print("Starting WebSocket server on ws://0.0.0.0:8765") + async with websockets.serve(tail_log, "0.0.0.0", 8765): + await asyncio.Future() # Run forever + + +if __name__ == "__main__": + asyncio.run(main())