From 9e39d4c7d2e7d3f944c167ff37dd15f4741538a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Tue, 2 Jun 2020 00:57:20 +0200 Subject: [PATCH] Implement IoT device and hub --- arduino/tempLightSensor/tempLightSensor.ino | 94 +++++++++++++++++++ docker/docker-compose.yml | 26 ++++- freedcs/api/__init__.py | 0 freedcs/api/admin.py | 32 +++++++ freedcs/api/apps.py | 5 + freedcs/api/migrations/0001_initial.py | 33 +++++++ .../api/migrations/0002_auto_20200601_1523.py | 21 +++++ freedcs/api/migrations/__init__.py | 0 freedcs/api/models.py | 35 +++++++ freedcs/api/serializers.py | 14 +++ freedcs/api/tests.py | 3 + freedcs/api/tests/sample.json | 1 + freedcs/api/urls.py | 23 +++++ freedcs/api/views.py | 22 +++++ freedcs/freedcs/settings.py | 15 ++- freedcs/freedcs/urls.py | 8 +- freedcs/telemetry/__init__.py | 0 freedcs/telemetry/admin.py | 7 ++ freedcs/telemetry/apps.py | 5 + freedcs/telemetry/migrations/0001_initial.py | 26 +++++ .../migrations/0002_auto_20200601_1557.py | 17 ++++ .../migrations/0003_auto_20200601_1710.py | 13 +++ freedcs/telemetry/migrations/__init__.py | 0 freedcs/telemetry/models.py | 17 ++++ freedcs/telemetry/serializers.py | 20 ++++ freedcs/telemetry/tests.py | 3 + freedcs/telemetry/tests/sample.json | 11 +++ freedcs/telemetry/urls.py | 23 +++++ freedcs/telemetry/views.py | 22 +++++ requirements.txt | 2 + 30 files changed, 492 insertions(+), 6 deletions(-) create mode 100644 arduino/tempLightSensor/tempLightSensor.ino create mode 100644 freedcs/api/__init__.py create mode 100644 freedcs/api/admin.py create mode 100644 freedcs/api/apps.py create mode 100644 freedcs/api/migrations/0001_initial.py create mode 100644 freedcs/api/migrations/0002_auto_20200601_1523.py create mode 100644 freedcs/api/migrations/__init__.py create mode 100644 freedcs/api/models.py create mode 100644 freedcs/api/serializers.py create mode 100644 freedcs/api/tests.py create mode 100644 freedcs/api/tests/sample.json create mode 100644 freedcs/api/urls.py create mode 100644 freedcs/api/views.py create mode 100644 freedcs/telemetry/__init__.py create mode 100644 freedcs/telemetry/admin.py create mode 100644 freedcs/telemetry/apps.py create mode 100644 freedcs/telemetry/migrations/0001_initial.py create mode 100644 freedcs/telemetry/migrations/0002_auto_20200601_1557.py create mode 100644 freedcs/telemetry/migrations/0003_auto_20200601_1710.py create mode 100644 freedcs/telemetry/migrations/__init__.py create mode 100644 freedcs/telemetry/models.py create mode 100644 freedcs/telemetry/serializers.py create mode 100644 freedcs/telemetry/tests.py create mode 100644 freedcs/telemetry/tests/sample.json create mode 100644 freedcs/telemetry/urls.py create mode 100644 freedcs/telemetry/views.py diff --git a/arduino/tempLightSensor/tempLightSensor.ino b/arduino/tempLightSensor/tempLightSensor.ino new file mode 100644 index 0000000..7b7b494 --- /dev/null +++ b/arduino/tempLightSensor/tempLightSensor.ino @@ -0,0 +1,94 @@ +#include +#include + +#define DEBUG_TO_SERIAL 1 +#define AREF_VOLTAGE 3.3 + +const String serverName = "sensor.server.domain"; + +const size_t capacity = JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(6); +DynamicJsonDocument doc(capacity); +JsonObject payload = doc.createNestedObject("payload"); +JsonObject temp = payload.createNestedObject("temperature"); + +int tempPin = A0; +int tempReading; +int photocellPin = A1; +int photocellReading; + +const byte mac[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; +const byte remoteAddr[] = { + 192, 168, 10, 123 }; +const int remotePort = 8000; +const int postDelay = 10*1000; + +const String serialNum = "abcde12345"; +const String URL = "/telemetry/"; + +void printAddr(byte addr[], Stream *stream) { + for (byte thisByte = 0; thisByte < 4; thisByte++) { + // print the value of each byte of the IP address: + stream->print(addr[thisByte], DEC); + if (thisByte < 3) { + stream->print("."); + } + } +} + +void setup(void) { + Serial.begin(9600); + + analogReference(EXTERNAL); + + if (Ethernet.begin(mac) == 0) { + Serial.println("Failed to configure Ethernet using DHCP"); + // no point in carrying on, so do nothing forevermore: + for(;;) + ; + } + + Serial.print("IoT started at address: "); + printAddr(Ethernet.localIP(), &Serial); + Serial.println(); + + doc["device"] = 1; // FIXME + payload["id"] = serverName; +} + +void loop(void) { + photocellReading = analogRead(photocellPin); + tempReading = analogRead(tempPin); + + float tempVoltage = tempReading * AREF_VOLTAGE / 1024.0; + float tempC = (tempVoltage - 0.5) * 100 ; + + payload["light"] = photocellReading; + + temp["celsius"] = tempC; + temp["raw"] = tempReading; + temp["volts"] = tempVoltage; + + if (EthernetClient client = client.connect(remoteAddr, remotePort)) { + client.print("POST "); + client.print(URL); + client.println(" HTTP/1.1"); + client.print("Host: "); + printAddr(remoteAddr, &client); + client.print(":"); + client.println(remotePort); + client.println("Content-Type: application/json"); + client.print("Content-Length: "); + client.println(measureJsonPretty(doc)); + client.println("Connection: close"); + client.println(); + serializeJson(doc, client); + client.stop(); + + #if DEBUG_TO_SERIAL + serializeJsonPretty(doc, Serial); + #endif + } + + delay(postDelay); +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index dcf2c80..5c17640 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1 +1,25 @@ -# Placeholder +version: "3.7" + +networks: + net: + +volumes: + pgdata: + +x-op-service-default: &service_default + restart: unless-stopped + init: true + +services: + timescale: + <<: *service_default + image: timescale/timescaledb:latest-pg12 + environment: + POSTGRES_USER: "timescale" + POSTGRES_PASSWORD: "password" + volumes: + - "pgdata:/var/lib/postgresql/data" + networks: + - net + ports: + - "127.0.0.1:5432:5432" diff --git a/freedcs/api/__init__.py b/freedcs/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freedcs/api/admin.py b/freedcs/api/admin.py new file mode 100644 index 0000000..b566819 --- /dev/null +++ b/freedcs/api/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin +from api.models import Device, WhiteList + + +@admin.register(Device) +class DeviceAdmin(admin.ModelAdmin): + readonly_fields = ('creation_time', 'updated_time',) + + fieldsets = ( + (None, { + 'fields': ('serial', ) + }), + ('Audit', { + 'classes': ('collapse',), + 'fields': ('creation_time', 'updated_time',) + }), + ) + + +@admin.register(WhiteList) +class WhiteListAdmin(admin.ModelAdmin): + readonly_fields = ('creation_time', 'updated_time',) + + fieldsets = ( + (None, { + 'fields': ('serial', 'is_published',) + }), + ('Audit', { + 'classes': ('collapse',), + 'fields': ('creation_time', 'updated_time',) + }), + ) diff --git a/freedcs/api/apps.py b/freedcs/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/freedcs/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/freedcs/api/migrations/0001_initial.py b/freedcs/api/migrations/0001_initial.py new file mode 100644 index 0000000..4282a77 --- /dev/null +++ b/freedcs/api/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.6 on 2020-06-01 14:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Device', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('serial', models.CharField(max_length=128, unique=True)), + ('creation_time', models.DateTimeField(auto_now_add=True)), + ('updated_time', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='WhiteList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('serial', models.CharField(max_length=128, unique=True)), + ('creation_time', models.DateTimeField(auto_now_add=True)), + ('updated_time', models.DateTimeField(auto_now=True)), + ('is_published', models.BooleanField(default=True)), + ], + ), + ] diff --git a/freedcs/api/migrations/0002_auto_20200601_1523.py b/freedcs/api/migrations/0002_auto_20200601_1523.py new file mode 100644 index 0000000..2d77c86 --- /dev/null +++ b/freedcs/api/migrations/0002_auto_20200601_1523.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.6 on 2020-06-01 15:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ['updated_time', 'serial']}, + ), + migrations.AlterModelOptions( + name='whitelist', + options={'ordering': ['serial', 'updated_time']}, + ), + ] diff --git a/freedcs/api/migrations/__init__.py b/freedcs/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freedcs/api/models.py b/freedcs/api/models.py new file mode 100644 index 0000000..6e22b08 --- /dev/null +++ b/freedcs/api/models.py @@ -0,0 +1,35 @@ +from django.db import models +from django.core.exceptions import ValidationError + + +def device_validation(value): + published_devices = WhiteList.objects.filter(serial=value, + is_published=True) + if not published_devices: + raise ValidationError("Device is not published") + + +class WhiteList(models.Model): + serial = models.CharField(max_length=128, unique=True) + creation_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) + is_published = models.BooleanField(default=True) + + class Meta: + ordering = ['serial', 'updated_time'] + + def __str__(self): + return self.serial + + +class Device(models.Model): + serial = models.CharField(max_length=128, unique=True, + validators=[device_validation]) + creation_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['updated_time', 'serial'] + + def __str__(self): + return self.serial diff --git a/freedcs/api/serializers.py b/freedcs/api/serializers.py new file mode 100644 index 0000000..cce3fbe --- /dev/null +++ b/freedcs/api/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from api.models import Device, WhiteList + + +class DeviceSerializer(serializers.ModelSerializer): + class Meta: + model = Device + fields = ('serial', 'creation_time', 'updated_time',) + + +# class WhiteListSerializer(serializers.ModelSerializer): +# class Meta: +# model = Device +# fields = ('serial', 'creation_time', 'updated_time',) diff --git a/freedcs/api/tests.py b/freedcs/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/freedcs/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/freedcs/api/tests/sample.json b/freedcs/api/tests/sample.json new file mode 100644 index 0000000..455d859 --- /dev/null +++ b/freedcs/api/tests/sample.json @@ -0,0 +1 @@ +{"serial": "abcde"} diff --git a/freedcs/api/urls.py b/freedcs/api/urls.py new file mode 100644 index 0000000..ad50dae --- /dev/null +++ b/freedcs/api/urls.py @@ -0,0 +1,23 @@ +"""freedcs URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path +from api.views import APISubscribe + +urlpatterns = [ + path('subscribe/', + APISubscribe.as_view({'get': 'list', 'post': 'create'}), + name='api_subscribe'), +] diff --git a/freedcs/api/views.py b/freedcs/api/views.py new file mode 100644 index 0000000..fc5a022 --- /dev/null +++ b/freedcs/api/views.py @@ -0,0 +1,22 @@ +from rest_framework.viewsets import ModelViewSet + +from api.models import Device +from api.serializers import DeviceSerializer + + +class APISubscribe(ModelViewSet): + queryset = Device.objects.all() + serializer_class = DeviceSerializer + + # def post(self, request): + # serializer = DeviceSerializer(data=request.data) + # if serializer.is_valid(): + # serializer.save() + # return Response(serializer.data, status=status.HTTP_201_CREATED) + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # def get(self, request): + # devices = Device.objects.all() + # import pdb; pdb.set_trace() + # serializer = DeviceSerializer(devices) + # return Response(serializer.serial) diff --git a/freedcs/freedcs/settings.py b/freedcs/freedcs/settings.py index 8e24c69..ff87f36 100644 --- a/freedcs/freedcs/settings.py +++ b/freedcs/freedcs/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = 'i4z%50+4b4ek(l0#!w2-r1hpo%&r6tk7p$p_-(=6d!c9n=g5m&' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition @@ -37,13 +37,16 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'api', + 'telemetry', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -75,8 +78,12 @@ WSGI_APPLICATION = 'freedcs.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'freedcs', + 'USER': 'freedcs', + 'PASSWORD': 'password', + 'HOST': '127.0.0.1', + 'PORT': '5432', } } diff --git a/freedcs/freedcs/urls.py b/freedcs/freedcs/urls.py index ae33b0a..1de2c27 100644 --- a/freedcs/freedcs/urls.py +++ b/freedcs/freedcs/urls.py @@ -13,9 +13,15 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin -from django.urls import path +from django.urls import include, path + +from api import urls as api_urls +from telemetry import urls as telemetry_urls urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include(api_urls)), + path('telemetry/', include(telemetry_urls)), ] diff --git a/freedcs/telemetry/__init__.py b/freedcs/telemetry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freedcs/telemetry/admin.py b/freedcs/telemetry/admin.py new file mode 100644 index 0000000..4ab6ccc --- /dev/null +++ b/freedcs/telemetry/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from telemetry.models import Telemetry + + +@admin.register(Telemetry) +class TelemetryAdmin(admin.ModelAdmin): + readonly_fields = ('device', 'time', 'payload',) diff --git a/freedcs/telemetry/apps.py b/freedcs/telemetry/apps.py new file mode 100644 index 0000000..99ef582 --- /dev/null +++ b/freedcs/telemetry/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TelemetryConfig(AppConfig): + name = 'telemetry' diff --git a/freedcs/telemetry/migrations/0001_initial.py b/freedcs/telemetry/migrations/0001_initial.py new file mode 100644 index 0000000..924cb81 --- /dev/null +++ b/freedcs/telemetry/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.6 on 2020-06-01 14:45 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Telemetry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True)), + ('payload', django.contrib.postgres.fields.jsonb.JSONField()), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Device')), + ], + ), + ] diff --git a/freedcs/telemetry/migrations/0002_auto_20200601_1557.py b/freedcs/telemetry/migrations/0002_auto_20200601_1557.py new file mode 100644 index 0000000..b6f83d2 --- /dev/null +++ b/freedcs/telemetry/migrations/0002_auto_20200601_1557.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.6 on 2020-06-01 15:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('telemetry', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='telemetry', + options={'ordering': ['time', 'device']}, + ), + ] diff --git a/freedcs/telemetry/migrations/0003_auto_20200601_1710.py b/freedcs/telemetry/migrations/0003_auto_20200601_1710.py new file mode 100644 index 0000000..a82d865 --- /dev/null +++ b/freedcs/telemetry/migrations/0003_auto_20200601_1710.py @@ -0,0 +1,13 @@ +# Generated by Django 3.0.6 on 2020-06-01 17:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('telemetry', '0002_auto_20200601_1557'), + ] + + operations = [ + ] diff --git a/freedcs/telemetry/migrations/__init__.py b/freedcs/telemetry/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freedcs/telemetry/models.py b/freedcs/telemetry/models.py new file mode 100644 index 0000000..6a6fd91 --- /dev/null +++ b/freedcs/telemetry/models.py @@ -0,0 +1,17 @@ +from django.db import models +from django.contrib.postgres.fields import JSONField + +from api.models import Device + + +class Telemetry(models.Model): + device = models.ForeignKey(Device, on_delete=models.CASCADE) + time = models.DateTimeField(auto_now_add=True) + payload = JSONField() + + class Meta: + ordering = ['time', 'device'] + verbose_name_plural = "Telemetry" + + def __str__(self): + return "%s - %s" % (self.time, self.device.serial) diff --git a/freedcs/telemetry/serializers.py b/freedcs/telemetry/serializers.py new file mode 100644 index 0000000..337012d --- /dev/null +++ b/freedcs/telemetry/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from api.serializers import DeviceSerializer +from telemetry.models import Telemetry + + +class TelemetrySerializer(serializers.ModelSerializer): + # device = DeviceSerializer(read_only=True) + + def validate(self, data): + return data + + class Meta: + model = Telemetry + fields = ('device', 'time', 'payload',) + + +# class WhiteListSerializer(serializers.ModelSerializer): +# class Meta: +# model = Device +# fields = ('serial', 'creation_time', 'updated_time',) diff --git a/freedcs/telemetry/tests.py b/freedcs/telemetry/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/freedcs/telemetry/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/freedcs/telemetry/tests/sample.json b/freedcs/telemetry/tests/sample.json new file mode 100644 index 0000000..2cf3097 --- /dev/null +++ b/freedcs/telemetry/tests/sample.json @@ -0,0 +1,11 @@ +{"device": 1, + "payload": { + "id": "sensor.server.domain", + "light": 434, + "temperature": { + "celsius": 27.02149, + "raw": 239, + "volts": 0.770215 + } + } +} diff --git a/freedcs/telemetry/urls.py b/freedcs/telemetry/urls.py new file mode 100644 index 0000000..0d3fe41 --- /dev/null +++ b/freedcs/telemetry/urls.py @@ -0,0 +1,23 @@ +"""freedcs URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path +from telemetry.views import Telemetry + +urlpatterns = [ + path('', + Telemetry.as_view({'get': 'list', 'post': 'create'}), + name='telemetry'), +] diff --git a/freedcs/telemetry/views.py b/freedcs/telemetry/views.py new file mode 100644 index 0000000..5bccf4a --- /dev/null +++ b/freedcs/telemetry/views.py @@ -0,0 +1,22 @@ +from rest_framework.viewsets import ModelViewSet + +from telemetry.models import Telemetry +from telemetry.serializers import TelemetrySerializer + + +class Telemetry(ModelViewSet): + queryset = Telemetry.objects.all() + serializer_class = TelemetrySerializer + + # def post(self, request): + # serializer = DeviceSerializer(data=request.data) + # if serializer.is_valid(): + # serializer.save() + # return Response(serializer.data, status=status.HTTP_201_CREATED) + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # def get(self, request): + # devices = Device.objects.all() + # import pdb; pdb.set_trace() + # serializer = DeviceSerializer(devices) + # return Response(serializer.serial) diff --git a/requirements.txt b/requirements.txt index 94a0e83..02650ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ Django +djangorestframework +psycopg2-binary