First working signal logger.
This commit is contained in:
commit
6c889380ec
|
|
@ -0,0 +1,18 @@
|
|||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory inside container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the Python script into the container
|
||||
COPY tcp_sia_server.py /app/
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Expose the port your server listens on
|
||||
EXPOSE 9000
|
||||
|
||||
# Run the script
|
||||
CMD ["python", "tcp_sia_server.py"]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
First working TCP SIA-DC09 logger. Logs to mariadb.
|
||||
|
||||
No frontend, just a backend with mariadb and a TCP server that parses SIA-DC09 protocol data.
|
||||
|
||||
|
||||
Current folder structure:
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── frontend
|
||||
│ └── index.html
|
||||
├── nginx.conf
|
||||
├── README.md
|
||||
├── requirements.txt
|
||||
├── tcp_sia_server.py
|
||||
└── tester.py
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.9
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_DATABASE: superarc
|
||||
MYSQL_USER: admin
|
||||
MYSQL_PASSWORD: yourpassword
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
restart: always
|
||||
|
||||
nginx:
|
||||
image: nginx:stable
|
||||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html:ro
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- mariadb
|
||||
- php-fpm
|
||||
|
||||
php-fpm:
|
||||
image: php:8.2-fpm
|
||||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html
|
||||
|
||||
tcp_sia_server:
|
||||
build: .
|
||||
ports:
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
working_dir: /app
|
||||
command: python -u tcp_sia_server.py
|
||||
depends_on:
|
||||
- mariadb
|
||||
|
||||
|
||||
volumes:
|
||||
mariadb_data:
|
||||
|
|
@ -0,0 +1 @@
|
|||
test
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
events { }
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.php index.html index.htm;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php-fpm:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
crccheck
|
||||
mysql-connector-python
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
import asyncio
|
||||
from crccheck.crc import CrcArc
|
||||
import re
|
||||
import json
|
||||
import mysql.connector
|
||||
from datetime import datetime
|
||||
|
||||
def verify_message(message: bytes) -> tuple[bool, str]:
|
||||
"""
|
||||
Verify the message format and CRC.
|
||||
Returns (True, payload_str) if valid, else (False, None)
|
||||
"""
|
||||
|
||||
if len(message) < 10:
|
||||
print("Message too short")
|
||||
return False, None
|
||||
|
||||
if message[0] != 10 or message[-1] != 13:
|
||||
print("Message must start with LF (\\n) and end with CR (\\r)")
|
||||
return False, None
|
||||
|
||||
crc_bytes = message[1:5] # 4 hex chars
|
||||
length_bytes = message[5:9] # 4 hex chars
|
||||
data_bytes = message[9:-1]
|
||||
|
||||
try:
|
||||
crc_str = crc_bytes.decode('ascii')
|
||||
length_str = length_bytes.decode('ascii')
|
||||
payload_str = data_bytes.decode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
print("Non-ASCII characters in message")
|
||||
return False, None
|
||||
|
||||
try:
|
||||
crc_recv = int(crc_str, 16)
|
||||
length_recv = int(length_str, 16)
|
||||
except ValueError:
|
||||
print("CRC or length not hex")
|
||||
return False, None
|
||||
|
||||
if length_recv != len(data_bytes):
|
||||
print(f"Length mismatch: declared {length_recv}, actual {len(data_bytes)}")
|
||||
return False, None
|
||||
|
||||
crc_calc = CrcArc.calc(data_bytes)
|
||||
if crc_calc != crc_recv:
|
||||
print(f"CRC mismatch: received {crc_str} calculated {crc_calc:04X}")
|
||||
return False, None
|
||||
|
||||
print(f"✅ Valid message received: {payload_str}")
|
||||
return True, payload_str
|
||||
|
||||
|
||||
def parse_sia_payload(payload: str) -> dict:
|
||||
"""
|
||||
Extracts fields from SIA-DC09 message according to the full protocol spec.
|
||||
Example:
|
||||
"SIA-DCS"0005L0#1234[1234|Nri1/RP00][Vimage.jpg][X010E57.5698][Y59N12.8358]
|
||||
"""
|
||||
|
||||
result = {
|
||||
"format": None,
|
||||
"seq": None,
|
||||
"line": None,
|
||||
"account": None,
|
||||
"receiver": None,
|
||||
"event": None,
|
||||
"zone": None,
|
||||
"partition": None,
|
||||
"code": None,
|
||||
"x": None,
|
||||
"y": None,
|
||||
"v": None,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
# Extract format and header
|
||||
header_match = re.match(r'"([^"]+)"(\d{4})L(\d)#(\d+)', payload)
|
||||
if header_match:
|
||||
result["format"] = header_match.group(1)
|
||||
result["seq"] = header_match.group(2)
|
||||
result["line"] = header_match.group(3)
|
||||
result["account"] = header_match.group(4)
|
||||
|
||||
# Main event block: [account|receiver/eventcode]
|
||||
event_match = re.search(r'\[\#?(\d+)\|([^\]]+)\]', payload)
|
||||
if event_match:
|
||||
result["account_verify"] = event_match.group(1)
|
||||
receiver_event = event_match.group(2)
|
||||
|
||||
# Split by slash
|
||||
if "/" in receiver_event:
|
||||
receiver, event = receiver_event.split("/", 1)
|
||||
result["receiver"] = receiver
|
||||
result["event"] = event
|
||||
|
||||
# Optional: split event into type (2 chars), zone (2–3 digits)
|
||||
event_code_match = re.match(r'([A-Z]{2})(\d+)', event)
|
||||
if event_code_match:
|
||||
result["code"] = event_code_match.group(1)
|
||||
result["zone"] = event_code_match.group(2)
|
||||
|
||||
# Extract partition from receiver (e.g., "Nri0" -> "0")
|
||||
partition_match = re.match(r'Nri(\d+)', receiver)
|
||||
if partition_match:
|
||||
result["partition"] = partition_match.group(1)
|
||||
|
||||
# Optional V block (usually image URL)
|
||||
v_match = re.search(r'\[V([^\]]*)\]', payload)
|
||||
if v_match:
|
||||
result["v"] = v_match.group(1)
|
||||
|
||||
# Optional GPS data
|
||||
x_match = re.search(r'\[X([^\]]+)\]', payload)
|
||||
y_match = re.search(r'\[Y([^\]]+)\]', payload)
|
||||
if x_match:
|
||||
result["x"] = x_match.group(1)
|
||||
if y_match:
|
||||
result["y"] = y_match.group(1)
|
||||
|
||||
time_match = re.search(r'_(\d{2}:\d{2}:\d{2}),(\d{2}-\d{2}-\d{4})', payload)
|
||||
if time_match:
|
||||
time_str = f"{time_match.group(2)} {time_match.group(1)}" # e.g., 07-25-2025 07:33:01
|
||||
result["signal_time"] = datetime.strptime(time_str, "%m-%d-%Y %H:%M:%S").isoformat()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
addr = writer.get_extra_info('peername')
|
||||
print(f"📡 Connection from {addr}")
|
||||
|
||||
try:
|
||||
data = await reader.readuntil(b'\r')
|
||||
print(f"📥 Raw received: {data}")
|
||||
|
||||
is_valid, payload = verify_message(data)
|
||||
response = b"\n0004NACK\r" # fallback
|
||||
|
||||
if is_valid and payload:
|
||||
parsed = parse_sia_payload(payload)
|
||||
parsed["source_ip"] = str(addr[0])
|
||||
print("📦 Parsed payload:")
|
||||
print(json.dumps(parsed, indent=2))
|
||||
sia_data = parsed
|
||||
# Connect to MariaDB
|
||||
conn = mysql.connector.connect(
|
||||
host="192.168.10.57",
|
||||
user="admin",
|
||||
password="yourpassword",
|
||||
database="superarc"
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
# Insert data
|
||||
query = """
|
||||
INSERT INTO signals (
|
||||
protocol, raw_message, account, sequence,
|
||||
line_number, event_code, `partition`, zone, signal_time, source_ip
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
values = (
|
||||
sia_data["format"],
|
||||
sia_data["raw"],
|
||||
sia_data.get("account"),
|
||||
sia_data.get("seq"),
|
||||
sia_data.get("line"),
|
||||
sia_data.get("code"),
|
||||
sia_data.get("partition"),
|
||||
sia_data.get("zone"),
|
||||
sia_data.get("signal_time"),
|
||||
sia_data.get("source_ip")
|
||||
)
|
||||
|
||||
cursor.execute(query, values)
|
||||
conn.commit()
|
||||
print("✅ Inserted signal into database.")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
seq = parsed.get("seq", "0000")
|
||||
account = parsed.get("account", "0000")
|
||||
|
||||
response_payload = f'"ACK"{seq}L0#{account}[]'
|
||||
crc = CrcArc.calc(response_payload.encode())
|
||||
length = str(hex(len(response_payload))).split('x')
|
||||
response = f"\n{crc:04X}00{length[1].upper()}{response_payload}\r".encode()
|
||||
|
||||
print(f"✅ Sending response: {response}")
|
||||
else:
|
||||
print("❌ Invalid message. Sending fallback NACK.")
|
||||
|
||||
writer.write(response)
|
||||
await writer.drain()
|
||||
|
||||
except asyncio.IncompleteReadError:
|
||||
print("⚠️ Connection closed before end of message")
|
||||
except Exception as e:
|
||||
print(f"🔥 Error: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
print(f"🔌 Connection with {addr} closed")
|
||||
|
||||
async def main():
|
||||
server = await asyncio.start_server(handle_client, host='0.0.0.0', port=9000)
|
||||
print("🚀 Listening on TCP port 9000 for SIA-DCS messages")
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import socket
|
||||
import os
|
||||
from crccheck.crc import CrcArc
|
||||
|
||||
def clear_screen():
|
||||
os.system("cls" if os.name == "nt" else "clear")
|
||||
|
||||
def send_tcp_message(host, port, message: bytes):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
print(f"📤 Sending: {message}")
|
||||
sock.sendall(message)
|
||||
response = sock.recv(1024)
|
||||
print(f"📥 Received: {response}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error sending message: {e}")
|
||||
|
||||
def build_sia_dcs_message():
|
||||
client_id = input("Enter Client ID (e.g. 1234): ").zfill(4)
|
||||
code = input("Enter Event Code (e.g. OP, RP, BA): ").upper()
|
||||
zone = input("Enter Zone (e.g. 01): ").zfill(2)
|
||||
image_url = input("Optional Image URL (or leave blank): ")
|
||||
|
||||
body = f'"SIA-DCS"0001L0#{client_id}[{client_id}|Nri1/{code}{zone}]'
|
||||
if image_url:
|
||||
body += f'[V{image_url}]'
|
||||
|
||||
data_bytes = body.encode("ascii")
|
||||
crc = CrcArc.calc(data_bytes)
|
||||
length = len(data_bytes)
|
||||
|
||||
full_message = f"\n{crc:04X}{length:04X}{body}\r".encode("ascii")
|
||||
return full_message
|
||||
|
||||
def build_adm_cid_message():
|
||||
line = input("Enter phone line number (e.g. 01): ").zfill(2)
|
||||
account = input("Enter account number (e.g. 1234): ").zfill(4)
|
||||
code = input("Enter event code (e.g. 401): ").zfill(3)
|
||||
qualifier = input("Enter qualifier (1=New, 3=Restore): ").zfill(1)
|
||||
area = input("Enter partition/area (e.g. 1): ").zfill(2)
|
||||
zone = input("Enter zone/user (e.g. 005): ").zfill(3)
|
||||
|
||||
# ADM-CID format: \r\nLLL:18QCCC[AAAA|EEMMZZ]\r
|
||||
cid_data = f'18{qualifier}{code}[{account}|{area}{area}{zone}]'
|
||||
msg = f"\r\n{line}{cid_data}\r".encode("ascii")
|
||||
return msg
|
||||
|
||||
def main():
|
||||
clear_screen()
|
||||
print("🚨 Alarm Test Sender")
|
||||
host = input("Enter receiver IP (e.g. 127.0.0.1): ")
|
||||
port = int(input("Enter port (e.g. 9000): "))
|
||||
|
||||
while True:
|
||||
print("\nChoose protocol:")
|
||||
print("1. Send SIA-DCS message")
|
||||
print("2. Send ADM-CID message")
|
||||
print("3. Exit")
|
||||
|
||||
choice = input("Select (1/2/3): ")
|
||||
|
||||
if choice == "1":
|
||||
msg = build_sia_dcs_message()
|
||||
send_tcp_message(host, port, msg)
|
||||
elif choice == "2":
|
||||
msg = build_adm_cid_message()
|
||||
send_tcp_message(host, port, msg)
|
||||
elif choice == "3":
|
||||
print("👋 Bye!")
|
||||
break
|
||||
else:
|
||||
print("Invalid choice")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue