mirror of
https://github.com/daniviga/bite.git
synced 2024-11-22 21:16:12 +01:00
Implement the MQTT protocol (#11)
* Add MQTT support on Arduino * Add MQTT simulator * Use a single python simulator * Minor improvements to dockerfiles * Add transport internal field to telemetry * Add PubSubClient
This commit is contained in:
parent
bbb92100f8
commit
a7cebefbf5
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -7,3 +7,6 @@
|
||||||
[submodule "arduino/libraries/NTPClient"]
|
[submodule "arduino/libraries/NTPClient"]
|
||||||
path = arduino/libraries/NTPClient
|
path = arduino/libraries/NTPClient
|
||||||
url = https://github.com/daniviga/NTPClient.git
|
url = https://github.com/daniviga/NTPClient.git
|
||||||
|
[submodule "arduino/libraries/pubsubclient"]
|
||||||
|
path = arduino/libraries/pubsubclient
|
||||||
|
url = https://github.com/knolleary/pubsubclient.git
|
||||||
|
|
1
arduino/libraries/pubsubclient
Submodule
1
arduino/libraries/pubsubclient
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2d228f2f862a95846c65a8518c79f48dfc8f188c
|
|
@ -1,12 +1,16 @@
|
||||||
#include <EEPROM.h>
|
#include <EEPROM.h>
|
||||||
#include <Ethernet.h>
|
#include <Ethernet.h>
|
||||||
#include <EthernetUdp.h>
|
#include <EthernetUdp.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
#include <NTPClient.h>
|
#include <NTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
#define DEBUG_TO_SERIAL 1
|
#define DEBUG_TO_SERIAL 1 // debug on serial port
|
||||||
|
#define USE_MQTT 0 // use mqtt protocol instead of http post
|
||||||
#define USE_INTERNAL_NTP 0 // use default ntp server or the internal one
|
#define USE_INTERNAL_NTP 0 // use default ntp server or the internal one
|
||||||
#define AREF_VOLTAGE 3.3
|
#define AREF_VOLTAGE 3.3 // set aref voltage to 3.3v instead of default 5v
|
||||||
|
|
||||||
|
char serial[9];
|
||||||
|
|
||||||
// const String serverName = "sensor.server.domain";
|
// const String serverName = "sensor.server.domain";
|
||||||
const size_t capacity = 2 * JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(2) + 20;
|
const size_t capacity = 2 * JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(2) + 20;
|
||||||
|
@ -17,11 +21,13 @@ JsonObject temp = payload.createNestedObject("temperature");
|
||||||
|
|
||||||
unsigned int counter = 0;
|
unsigned int counter = 0;
|
||||||
|
|
||||||
|
|
||||||
EthernetUDP ntpUDP;
|
EthernetUDP ntpUDP;
|
||||||
NTPClient timeClient(ntpUDP);
|
NTPClient timeClient(ntpUDP);
|
||||||
bool NTPValid = false;
|
bool NTPValid = false;
|
||||||
|
|
||||||
|
EthernetClient ethClient;
|
||||||
|
PubSubClient clientMQTT(ethClient);
|
||||||
|
|
||||||
struct netConfig {
|
struct netConfig {
|
||||||
IPAddress address;
|
IPAddress address;
|
||||||
unsigned int port;
|
unsigned int port;
|
||||||
|
@ -39,8 +45,6 @@ void setup(void) {
|
||||||
StaticJsonDocument<20> api;
|
StaticJsonDocument<20> api;
|
||||||
|
|
||||||
byte mac[6];
|
byte mac[6];
|
||||||
char serial[9];
|
|
||||||
|
|
||||||
int eeAddress = 0;
|
int eeAddress = 0;
|
||||||
|
|
||||||
EEPROM.get(eeAddress, mac);
|
EEPROM.get(eeAddress, mac);
|
||||||
|
@ -60,10 +64,10 @@ void setup(void) {
|
||||||
|
|
||||||
Serial.print("IoT #");
|
Serial.print("IoT #");
|
||||||
Serial.print(serial);
|
Serial.print(serial);
|
||||||
Serial.println(" at address:");
|
Serial.print(" at address: ");
|
||||||
Serial.println(Ethernet.localIP());
|
Serial.println(Ethernet.localIP());
|
||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.println("Connecting to:");
|
Serial.print("Connecting to: ");
|
||||||
Serial.print(config.address);
|
Serial.print(config.address);
|
||||||
Serial.print(":");
|
Serial.print(":");
|
||||||
Serial.println(config.port);
|
Serial.println(config.port);
|
||||||
|
@ -85,6 +89,10 @@ void setup(void) {
|
||||||
|
|
||||||
telemetry["device"] = serial;
|
telemetry["device"] = serial;
|
||||||
// payload["id"] = serverName;
|
// payload["id"] = serverName;
|
||||||
|
|
||||||
|
#if USE_MQTT
|
||||||
|
clientMQTT.setServer(config.address, 1883);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop(void) {
|
void loop(void) {
|
||||||
|
@ -107,7 +115,11 @@ void loop(void) {
|
||||||
temp["raw"] = tempReading;
|
temp["raw"] = tempReading;
|
||||||
temp["volts"] = tempVoltage;
|
temp["volts"] = tempVoltage;
|
||||||
|
|
||||||
|
#if USE_MQTT
|
||||||
|
publishData(config, telemetry);
|
||||||
|
#else
|
||||||
postData(config, telemetryURL, telemetry);
|
postData(config, telemetryURL, telemetry);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (counter == 6 * 120) { // Update clock every 6 times * 10 sec * 120 minutes = 2 hrs
|
if (counter == 6 * 120) { // Update clock every 6 times * 10 sec * 120 minutes = 2 hrs
|
||||||
timeClient.update();
|
timeClient.update();
|
||||||
|
@ -122,6 +134,22 @@ void loop(void) {
|
||||||
delay(postDelay);
|
delay(postDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if USE_MQTT
|
||||||
|
void publishData(const netConfig &mqtt, const DynamicJsonDocument &json) {
|
||||||
|
if (clientMQTT.connect(serial, "freedcs", "password")) {
|
||||||
|
char buffer[256];
|
||||||
|
serializeJson(json, buffer);
|
||||||
|
clientMQTT.publish(serial, buffer);
|
||||||
|
|
||||||
|
#if DEBUG_TO_SERIAL
|
||||||
|
Serial.println("DEBUG: MQTT PUBLISH>>>");
|
||||||
|
serializeJsonPretty(json, Serial);
|
||||||
|
Serial.println("\n<<<");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void postData(const netConfig &postAPI, const String &URL, const DynamicJsonDocument &json) {
|
void postData(const netConfig &postAPI, const String &URL, const DynamicJsonDocument &json) {
|
||||||
if (EthernetClient client = client.connect(postAPI.address, postAPI.port)) {
|
if (EthernetClient client = client.connect(postAPI.address, postAPI.port)) {
|
||||||
client.print("POST ");
|
client.print("POST ");
|
||||||
|
@ -140,7 +168,7 @@ void postData(const netConfig &postAPI, const String &URL, const DynamicJsonDocu
|
||||||
client.stop();
|
client.stop();
|
||||||
|
|
||||||
#if DEBUG_TO_SERIAL
|
#if DEBUG_TO_SERIAL
|
||||||
Serial.println("DEBUG: >>>");
|
Serial.println("DEBUG: HTTP POST>>>");
|
||||||
serializeJsonPretty(json, Serial);
|
serializeJsonPretty(json, Serial);
|
||||||
Serial.println("\n<<<");
|
Serial.println("\n<<<");
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -7,12 +7,14 @@ volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|
||||||
x-op-service-default: &service_default
|
x-op-service-default: &service_default
|
||||||
restart: unless-stopped
|
restart: always # unless-stopped
|
||||||
init: true
|
init: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ntpd:
|
ntpd:
|
||||||
<<: *service_default
|
<<: *service_default
|
||||||
|
build:
|
||||||
|
context: ./ntpd
|
||||||
image: daniviga/ntpd
|
image: daniviga/ntpd
|
||||||
networks:
|
networks:
|
||||||
- net
|
- net
|
||||||
|
@ -32,18 +34,35 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:5432:5432"
|
- "127.0.0.1:5432:5432"
|
||||||
|
|
||||||
|
# mosquitto simple deployment
|
||||||
|
# mqtt:
|
||||||
|
# <<: *service_default
|
||||||
|
# # image: vernemq/vernemq
|
||||||
|
# # environment:
|
||||||
|
# # DOCKER_VERNEMQ_ALLOW_ANONYMOUS: "on"
|
||||||
|
# # DOCKER_VERNEMQ_ACCEPT_EULA: "yes"
|
||||||
|
# image: eclipse-mosquitto
|
||||||
|
# networks:
|
||||||
|
# - net
|
||||||
|
# ports:
|
||||||
|
# - "1883:1883"
|
||||||
|
# # - "9001:9001" # mqtt via websocket
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
<<: *service_default
|
<<: *service_default
|
||||||
image: rabbitmq:3-management
|
image: rabbitmq:3-management-alpine
|
||||||
environment:
|
# environment: # we use unauth access atm
|
||||||
RABBITMQ_DEFAULT_VHOST: "freedcs"
|
# RABBITMQ_DEFAULT_VHOST: "freedcs"
|
||||||
RABBITMQ_DEFAULT_USER: "freedcs"
|
# RABBITMQ_DEFAULT_USER: "freedcs"
|
||||||
RABBITMQ_DEFAULT_PASS: "password"
|
# RABBITMQ_DEFAULT_PASS: "password"
|
||||||
|
volumes:
|
||||||
|
- ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
|
||||||
networks:
|
networks:
|
||||||
- net
|
- net
|
||||||
ports:
|
ports:
|
||||||
- "15672:15672"
|
- "1883:1883"
|
||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
|
||||||
edge:
|
edge:
|
||||||
<<: *service_default
|
<<: *service_default
|
||||||
|
|
|
@ -4,17 +4,35 @@ networks:
|
||||||
localnet:
|
localnet:
|
||||||
|
|
||||||
x-op-service-default: &service_default
|
x-op-service-default: &service_default
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
init: true
|
init: true
|
||||||
|
tty: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
device-http:
|
device-http:
|
||||||
<<: *service_default
|
<<: *service_default
|
||||||
image: daniviga/freedcs-device-http
|
build:
|
||||||
|
context: ../simulator
|
||||||
|
image: daniviga/freedcs-device-simulator
|
||||||
environment:
|
environment:
|
||||||
IOT_HOST: "http://192.168.10.123:8000"
|
IOT_HTTP: "http://192.168.10.123:8000"
|
||||||
# IOT_SERIAL: "abcd1234"
|
# IOT_SERIAL: "abcd1234"
|
||||||
# IOT_DELAY: 10
|
# IOT_DELAY: 10
|
||||||
IOT_DEBUG: 1
|
IOT_DEBUG: 1
|
||||||
networks:
|
networks:
|
||||||
- localnet
|
- localnet
|
||||||
|
|
||||||
|
device-mqtt:
|
||||||
|
<<: *service_default
|
||||||
|
build:
|
||||||
|
context: ../simulator
|
||||||
|
image: daniviga/freedcs-device-simulator
|
||||||
|
environment:
|
||||||
|
IOT_HTTP: "http://192.168.10.123:8000"
|
||||||
|
IOT_MQTT: "192.168.10.123:1883"
|
||||||
|
# IOT_SERIAL: "abcd1234"
|
||||||
|
# IOT_DELAY: 10
|
||||||
|
IOT_DEBUG: 1
|
||||||
|
command: ["/opt/freedcs/device_simulator.py", "-t", "mqtt"]
|
||||||
|
networks:
|
||||||
|
- localnet
|
||||||
|
|
1
docker/rabbitmq/enabled_plugins
Normal file
1
docker/rabbitmq/enabled_plugins
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[rabbitmq_management,rabbitmq_mqtt].
|
7
docker/simulator/Dockerfile
Normal file
7
docker/simulator/Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM python:3.8-alpine
|
||||||
|
|
||||||
|
RUN pip3 install urllib3 paho-mqtt
|
||||||
|
COPY ./device_simulator.py /opt/freedcs/device_simulator.py
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3"]
|
||||||
|
CMD ["/opt/freedcs/device_simulator.py"]
|
115
docker/simulator/device_simulator.py
Normal file
115
docker/simulator/device_simulator.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
import datetime
|
||||||
|
import urllib3
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
import paho.mqtt.publish as publish
|
||||||
|
|
||||||
|
DEBUG = bool(os.environ.get('IOT_DEBUG', False))
|
||||||
|
http = urllib3.PoolManager()
|
||||||
|
|
||||||
|
|
||||||
|
def post_json(endpoint, url, data):
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print(json_data)
|
||||||
|
|
||||||
|
encoded_data = json_data.encode('utf8')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
r = http.request(
|
||||||
|
'POST',
|
||||||
|
endpoint + url,
|
||||||
|
body=encoded_data,
|
||||||
|
headers={'content-type': 'application/json'})
|
||||||
|
return r
|
||||||
|
except urllib3.exceptions.MaxRetryError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sleep(10) # retry in 10 seconds
|
||||||
|
|
||||||
|
|
||||||
|
def publish_json(endpoint, data):
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
serial = data['device']
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print(json_data)
|
||||||
|
|
||||||
|
encoded_data = json_data.encode('utf8')
|
||||||
|
|
||||||
|
publish.single(
|
||||||
|
topic=serial,
|
||||||
|
payload=encoded_data,
|
||||||
|
hostname=endpoint.split(':')[0],
|
||||||
|
port=int(endpoint.split(':')[1]),
|
||||||
|
client_id=serial,
|
||||||
|
# auth=auth FIXME
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='IoT simulator oprions')
|
||||||
|
|
||||||
|
parser.add_argument('-e', '--endpoint',
|
||||||
|
default=os.environ.get('IOT_HTTP',
|
||||||
|
'http://127.0.0.1:8000'),
|
||||||
|
help='IoT HTTP endpoint')
|
||||||
|
parser.add_argument('-m', '--mqtt',
|
||||||
|
default=os.environ.get('IOT_MQTT',
|
||||||
|
'127.0.0.1:1883'),
|
||||||
|
help='IoT MQTT endpoint')
|
||||||
|
parser.add_argument('-t', '--transport',
|
||||||
|
choices=['mqtt', 'http'],
|
||||||
|
default=os.environ.get('IOT_TL', 'http'),
|
||||||
|
help='IoT transport layer')
|
||||||
|
parser.add_argument('-s', '--serial',
|
||||||
|
default=os.environ.get('IOT_SERIAL'),
|
||||||
|
help='IoT device serial number')
|
||||||
|
parser.add_argument('-d', '--delay', metavar='s', type=int,
|
||||||
|
default=os.environ.get('IOT_DELAY', 10),
|
||||||
|
help='Delay between requests')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
subscribe = '/api/device/subscribe/'
|
||||||
|
telemetry = '/telemetry/'
|
||||||
|
|
||||||
|
if args.serial is None:
|
||||||
|
args.serial = ''.join(
|
||||||
|
random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||||
|
|
||||||
|
data = {'serial': args.serial}
|
||||||
|
post_json(args.endpoint, subscribe, data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'device': args.serial,
|
||||||
|
'clock': int(datetime.datetime.now().timestamp()),
|
||||||
|
}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
payload = {
|
||||||
|
'id': 'device_simulator',
|
||||||
|
'light': random.randint(300, 500),
|
||||||
|
'temperature': {
|
||||||
|
'celsius': round(random.uniform(20, 28), 1)}
|
||||||
|
}
|
||||||
|
if args.transport == 'http':
|
||||||
|
post_json(args.endpoint, telemetry, {**data, 'payload': payload})
|
||||||
|
elif args.transport == 'mqtt':
|
||||||
|
publish_json(args.mqtt, {**data, 'payload': payload})
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
sleep(args.delay)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -1,6 +0,0 @@
|
||||||
FROM python:3.8-alpine
|
|
||||||
|
|
||||||
RUN pip3 install urllib3
|
|
||||||
COPY ./simulator_http.py /opt/freedcs/simulator_http.py
|
|
||||||
|
|
||||||
ENTRYPOINT ["python3", "/opt/freedcs/simulator_http.py"]
|
|
|
@ -1,68 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import string
|
|
||||||
import random
|
|
||||||
import datetime
|
|
||||||
import urllib3
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
DEBUG = bool(os.environ.get('IOT_DEBUG', False))
|
|
||||||
http = urllib3.PoolManager()
|
|
||||||
|
|
||||||
|
|
||||||
def post_json(host, url, data):
|
|
||||||
json_data = json.dumps(data)
|
|
||||||
|
|
||||||
if DEBUG:
|
|
||||||
print(json_data)
|
|
||||||
|
|
||||||
encoded_data = json_data.encode('utf8')
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
r = http.request(
|
|
||||||
'POST',
|
|
||||||
host + url,
|
|
||||||
body=encoded_data,
|
|
||||||
headers={'content-type': 'application/json'})
|
|
||||||
return r
|
|
||||||
except urllib3.exceptions.MaxRetryError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sleep(10) # retry in 10 seconds
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
host = os.environ.get('IOT_HOST', 'http://127.0.0.1:8000')
|
|
||||||
subscribe = '/api/device/subscribe/'
|
|
||||||
telemetry = '/telemetry/'
|
|
||||||
delay = int(os.environ.get('IOT_DELAY', 10))
|
|
||||||
|
|
||||||
serial = os.environ.get('IOT_SERIAL')
|
|
||||||
if serial is None:
|
|
||||||
serial = ''.join(
|
|
||||||
random.choices(string.ascii_lowercase + string.digits, k=8))
|
|
||||||
|
|
||||||
data = {'serial': serial}
|
|
||||||
post_json(host, subscribe, data)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'device': serial,
|
|
||||||
'clock': int(datetime.datetime.now().timestamp()),
|
|
||||||
}
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payload = {
|
|
||||||
'id': 'device_http_simulator',
|
|
||||||
'light': random.randint(300, 500),
|
|
||||||
'temperature': {
|
|
||||||
'celsius': round(random.uniform(20, 28), 1)}
|
|
||||||
}
|
|
||||||
post_json(host, telemetry, {**data, 'payload': payload})
|
|
||||||
sleep(delay)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
18
freedcs/telemetry/migrations/0007_telemetry_transport.py
Normal file
18
freedcs/telemetry/migrations/0007_telemetry_transport.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.6 on 2020-06-08 20:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('telemetry', '0006_auto_20200603_1317'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='telemetry',
|
||||||
|
name='transport',
|
||||||
|
field=models.CharField(choices=[('http', 'http'), ('mqtt', 'mqtt')], default='http', max_length=4),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,9 @@ from api.models import Device
|
||||||
class Telemetry(models.Model):
|
class Telemetry(models.Model):
|
||||||
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
||||||
time = models.DateTimeField(primary_key=True, auto_now_add=True)
|
time = models.DateTimeField(primary_key=True, auto_now_add=True)
|
||||||
|
transport = models.CharField(max_length=4,
|
||||||
|
choices=[('http', 'http'), ('mqtt', 'mqtt')],
|
||||||
|
default='http')
|
||||||
clock = models.IntegerField(
|
clock = models.IntegerField(
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(0)],
|
||||||
null=True)
|
null=True)
|
||||||
|
|
|
@ -11,4 +11,10 @@ class TelemetrySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Telemetry
|
model = Telemetry
|
||||||
fields = ('time', 'device', 'clock', 'payload',)
|
fields = ('time', 'device', 'clock', 'transport', 'payload',)
|
||||||
|
read_only_fields = ['transport']
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['transport'] = 'http'
|
||||||
|
telemetry = Telemetry.objects.create(**validated_data)
|
||||||
|
return telemetry
|
||||||
|
|
Loading…
Reference in New Issue
Block a user