mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-04 13:17:50 +02:00
Rename DCC project into RAM
RAM: Railroad Assets Manager
This commit is contained in:
0
ram/driver/__init__.py
Normal file
0
ram/driver/__init__.py
Normal file
29
ram/driver/admin.py
Normal file
29
ram/driver/admin.py
Normal 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
12
ram/driver/apps.py
Normal 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
53
ram/driver/connector.py
Normal 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
22
ram/driver/health.py
Normal 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"
|
26
ram/driver/migrations/0001_initial.py
Normal file
26
ram/driver/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
0
ram/driver/migrations/__init__.py
Normal file
0
ram/driver/migrations/__init__.py
Normal file
41
ram/driver/models.py
Normal file
41
ram/driver/models.py
Normal 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
19
ram/driver/serializers.py
Normal 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
3
ram/driver/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
11
ram/driver/urls.py
Normal file
11
ram/driver/urls.py
Normal 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
162
ram/driver/views.py
Normal 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
|
||||
)
|
Reference in New Issue
Block a user