ionescu77 / my-channels-play-pub

MVP Django Channels 2 and WebSocket pure JS
0 stars 0 forks source link

ZMQ: Django channels zmq mqtt vue #3

Open ionescu77 opened 3 years ago

ionescu77 commented 3 years ago

https://stackoverflow.com/questions/58832526/is-it-possible-to-use-zeromq-sockets-in-a-django-channels-consumer

I've got a hobby project of building an autonomous boat. I now built a GUI using a Vuejs frontend and a Django backend. In this GUI I can see the boat on a map, and send commands to it. Those commands are sent over ZeroMQ sockets which works great.

I'm using Django channels to send the commands from the frontend over a websocket to the backend and from there I send it on over the ZeroMQ socket. My consumer (which works great) looks as follows:

import zmq
from channels.generic.websocket import WebsocketConsumer
from .tools import get_vehicle_ip_address, get_vehicle_steer_socket

context = zmq.Context()

class SteerConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.forward_steer_socket = get_vehicle_steer_socket(context, get_vehicle_ip_address())

    def connect(self):
        self.accept()

    def receive(self, text_data):
        print("Passing on the commands from the frontend:", text_data, "to the boat")
        self.forward_steer_socket.send_string(text_data)

Next to this I also receive location information from the boat over a ZeroMQ socket which I save to the database. I'm running this in a separate script and the frontend simply polls the backend every 2 seconds for updates. Here's the script receiving the boat info:

import os
import django
import zmq
os.environ['DJANGO_SETTINGS_MODULE'] = 'server.settings'
django.setup()

# Socket to receive the boat location
context = zmq.Context()
location_socket = context.socket(zmq.SUB)
location_socket.setsockopt(zmq.CONFLATE, True)
location_socket.bind('tcp://*:6001')
location_socket.setsockopt_string(zmq.SUBSCRIBE, '')

while True:
    boat_location = location_socket.recv_json()
    print(boat_location)
    # HERE I STORE THE BOAT LOCATION in the DB

I would now like to add this location_socket to the Consumer so that the Consumer can also receive the boat location on the ZeroMQ socket and send it to the frontend over the websocket.

I can of course simply add the location_socket to the Consumer its __init__() method as follows:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.forward_steer_socket = get_vehicle_steer_socket(context, get_vehicle_ip_address())

    self.location_socket = context.socket(zmq.SUB)
    self.location_socket.setsockopt(zmq.CONFLATE, True)
    self.location_socket.bind('tcp://*:6001')
    self.location_socket.setsockopt_string(zmq.SUBSCRIBE, '')

But I obviously cannot include the while True loop in the Consumer. So from here I'm not sure what to do. I actually don't know whether this is even possible, since Django Channels seems to specifically been made for websockets. I guess I could start using multithreading or multiprocessing libraries, but that is uncharted territory for me.

Does anybody know whether and how it is possible to make a ZeroMQ listener in a Django Channel?

ionescu77 commented 3 years ago

It is possible to send message to your consumer directly from your separated script via: https://channels.readthedocs.io/en/latest/topics/channel_layers.html#using-outside-of-consumers

When the new client connects to your consumer inside SteerConsumer you have self.channel_name which is unique for that client. To send message to that consumer you just have to execute (in your example from separated script):

from channels.layers import get_channel_layer

channel_layer = get_channel_layer()
# "channel_name" should be replaced for the proper name of course
channel_layer.send("channel_name", {
    "type": "chat.message",
    "text": "Hello there!",
})

and add inside your SteerConsumer method to handle this message:

def chat_message(self, event):
    # Handles the "chat.message" event when it's sent to us.
    self.send(text_data=event["text"])