Rename DCC project into RAM

RAM: Railroad Assets Manager
This commit is contained in:
2022-04-10 21:05:02 +02:00
parent 305507a4e6
commit d594dbe47c
77 changed files with 16 additions and 16 deletions

0
ram/driver/__init__.py Normal file
View File

29
ram/driver/admin.py Normal file
View File

@@ -0,0 +1,29 @@
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from driver.models import DriverConfiguration
@admin.register(DriverConfiguration)
class DriverConfigurationAdmin(SingletonModelAdmin):
fieldsets = (
(
"Remote DCC-EX configuration",
{
"fields": (
"remote_host",
"remote_port",
"timeout",
)
},
),
(
"Firewall setting",
{
"fields": (
"network",
"subnet_mask",
)
},
),
)

12
ram/driver/apps.py Normal file
View File

@@ -0,0 +1,12 @@
from django.apps import AppConfig
from health_check.plugins import plugin_dir
class DriverConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "driver"
def ready(self):
from driver.health import DriverHealthCheck
plugin_dir.register(DriverHealthCheck)

53
ram/driver/connector.py Normal file
View File

@@ -0,0 +1,53 @@
import socket
from driver.models import DriverConfiguration
class Connector:
def __init__(self):
self.config = DriverConfiguration.get_solo()
def __send_data(self, message):
resp = b""
# convert to binary if str is received
if isinstance(message, str):
message = message.encode()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((self.config.remote_host, self.config.remote_port))
sock.settimeout(self.config.timeout / 1000) # milliseconds
sock.sendall(message)
while True:
try:
resp += sock.recv(1024)
except socket.timeout:
break
return resp
def passthrough(self, data):
return self.__send_data(data)
def ops(self, address, data, function=False):
if function:
message = "<F {0} {1} {2}>".format(
address, data["function"], data["state"]
)
else:
message = "<t 1 {0} {1} {2}>".format(
address, data["speed"], data["direction"]
)
self.__send_data(message)
def infra(self, data):
if "track" in data:
track = " {}".format(data["track"].upper())
else:
track = ""
if data["power"]:
self.__send_data("<1{}>".format(track))
else:
self.__send_data("<0{}>".format(track))
def emergency(self):
self.__send_data("<!>")

22
ram/driver/health.py Normal file
View File

@@ -0,0 +1,22 @@
from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import (
ServiceUnavailable,
ServiceReturnedUnexpectedResult,
)
from driver.connector import Connector
class DriverHealthCheck(BaseHealthCheckBackend):
critical_service = False
def check_status(self):
try:
Connector().passthrough(b"<s>")
except ConnectionRefusedError as e:
self.add_error(ServiceUnavailable("IOError"), e)
except Exception as e:
self.add_error(ServiceReturnedUnexpectedResult("IOError"), e)
def identifier(self):
return "DriverDaemon"

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.0.3 on 2022-04-07 09:25
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DriverConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('remote_host', models.GenericIPAddressField(default='192.168.4.1', protocol='IPv4')),
('remote_port', models.SmallIntegerField(default=2560)),
('timeout', models.SmallIntegerField(default=250)),
],
options={
'verbose_name': 'Configuration',
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.0.3 on 2022-04-10 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='driverconfiguration',
name='network',
field=models.GenericIPAddressField(default='192.168.4.0', protocol='IPv4'),
),
migrations.AddField(
model_name='driverconfiguration',
name='subnet_mask',
field=models.GenericIPAddressField(default='255.255.255.0', protocol='IPv4'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.0.3 on 2022-04-10 16:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0002_driverconfiguration_network_and_more'),
]
operations = [
migrations.AlterField(
model_name='driverconfiguration',
name='network',
field=models.GenericIPAddressField(blank=True, default='192.168.4.0', null=True, protocol='IPv4'),
),
migrations.AlterField(
model_name='driverconfiguration',
name='remote_host',
field=models.GenericIPAddressField(blank=True, default='192.168.4.1', null=True, protocol='IPv4'),
),
migrations.AlterField(
model_name='driverconfiguration',
name='subnet_mask',
field=models.GenericIPAddressField(blank=True, default='255.255.255.0', null=True, protocol='IPv4'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.3 on 2022-04-10 16:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0003_alter_driverconfiguration_network_and_more'),
]
operations = [
migrations.AlterField(
model_name='driverconfiguration',
name='remote_host',
field=models.GenericIPAddressField(default='192.168.4.1', protocol='IPv4'),
),
]

View File

41
ram/driver/models.py Normal file
View File

@@ -0,0 +1,41 @@
from django.db import models
from django.core.exceptions import ValidationError
from ipaddress import IPv4Address, IPv4Network
from solo.models import SingletonModel
class DriverConfiguration(SingletonModel):
remote_host = models.GenericIPAddressField(
protocol="IPv4",
default="192.168.4.1"
)
remote_port = models.SmallIntegerField(default=2560)
timeout = models.SmallIntegerField(default=250)
network = models.GenericIPAddressField(
protocol="IPv4",
default="192.168.4.0",
blank=True,
null=True
)
subnet_mask = models.GenericIPAddressField(
protocol="IPv4",
default="255.255.255.0",
blank=True,
null=True
)
def __str__(self):
return "Configuration"
def clean(self, *args, **kwargs):
if self.network:
try:
IPv4Network(
"{0}/{1}".format(self.network, self.subnet_mask))
except ValueError as e:
raise ValidationError(e)
super().clean(*args, **kwargs)
class Meta:
verbose_name = "Configuration"

19
ram/driver/serializers.py Normal file
View File

@@ -0,0 +1,19 @@
from rest_framework import serializers
class FunctionSerializer(serializers.Serializer):
function = serializers.IntegerField(required=True)
state = serializers.IntegerField(required=True)
class CabSerializer(serializers.Serializer):
speed = serializers.IntegerField(required=True)
direction = serializers.IntegerField(required=True)
class InfraSerializer(serializers.Serializer):
power = serializers.BooleanField(required=True)
track = serializers.ChoiceField(
choices=("main", "prog", "join", "MAIN", "PROG", "JOIN"),
required=False,
)

3
ram/driver/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
ram/driver/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from driver.views import SendCommand, Function, Cab, Emergency, Infra, Test
urlpatterns = [
path("test", Test.as_view()),
path("emergency", Emergency.as_view()),
path("infra", Infra.as_view()),
path("command", SendCommand.as_view()),
path("<int:address>/cab", Cab.as_view()),
path("<int:address>/function", Function.as_view()),
]

162
ram/driver/views.py Normal file
View File

@@ -0,0 +1,162 @@
from ipaddress import IPv4Address, IPv4Network
from django.http import Http404
from django.utils.decorators import method_decorator
from rest_framework import status, serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import (
IsAuthenticated,
BasePermission,
SAFE_METHODS
)
from ram.parsers import PlainTextParser
from driver.models import DriverConfiguration
from driver.connector import Connector
from driver.serializers import (
FunctionSerializer,
CabSerializer,
InfraSerializer,
)
from roster.models import RollingStock
def addresschecker(f):
"""
Check if DCC address does exist in the database
"""
def addresslookup(request, address, *args):
if not RollingStock.objects.filter(address=address):
raise Http404
return f(request, address, *args)
return addresslookup
class Firewall(BasePermission):
def has_permission(self, request, view):
config = DriverConfiguration.get_solo()
if not config.network:
return request.method in SAFE_METHODS
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = IPv4Address(x_forwarded_for.split(',')[0])
else:
ip = IPv4Address(request.META.get("REMOTE_ADDR"))
network = IPv4Network("{0}/{1}".format(
config.network,
config.subnet_mask
))
# accept IP configured is settings or localhost
if ip in network or ip in IPv4Network("127.0.0.0/8"):
return request.method in SAFE_METHODS
class Test(APIView):
"""
Send a test <s> command
"""
parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall]
def get(self, request):
response = Connector().passthrough("<s>")
return Response(
{"response": response.decode()}, status=status.HTTP_202_ACCEPTED
)
class SendCommand(APIView):
"""
Command passthrough
"""
parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall]
def put(self, request):
data = request.data
if not data:
raise serializers.ValidationError(
{"error": "a string is expected"}
)
cmd = data.decode().strip()
if not (cmd.startswith("<") and cmd.endswith(">")):
raise serializers.ValidationError(
{"error": "please provide a valid command"}
)
response = Connector().passthrough(cmd)
return Response(
{"response": response.decode()}, status=status.HTTP_202_ACCEPTED
)
@method_decorator(addresschecker, name="put")
class Function(APIView):
"""
Send "Function" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
def put(self, request, address):
serializer = FunctionSerializer(data=request.data)
if serializer.is_valid():
Connector().ops(address, serializer.data, function=True)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(addresschecker, name="put")
class Cab(APIView):
"""
Send "Cab" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
def put(self, request, address):
serializer = CabSerializer(data=request.data)
if serializer.is_valid():
Connector().ops(address, serializer.data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class Infra(APIView):
"""
Send "Infra" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
def put(self, request):
serializer = InfraSerializer(data=request.data)
if serializer.is_valid():
Connector().infra(serializer.data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class Emergency(APIView):
"""
Send an "Emergency" stop, no matter the HTTP method used
"""
permission_classes = [IsAuthenticated | Firewall]
def put(self, request):
Connector().emergency()
return Response(
{"response": "emergency stop"}, status=status.HTTP_202_ACCEPTED
)
def get(self, request):
Connector().emergency()
return Response(
{"response": "emergency stop"}, status=status.HTTP_202_ACCEPTED
)