Elixir Cowboy Websocket - 위도 경도 근접성으로 IOT 장치 쿼리

33032 단어
우리의 문제 목적 진술에서 우리는
알려진 위도와 경도 위치를 가진 많은 장치는 위치의 변화를 지속적으로 알려줍니다.

우리의 목표는 주어진 위치 근처에 있는 장치에 메시지를 보내는 것입니다.
또는 한 장치가 주어진 반경 내에서 가까운 장치가 누구인지 알아야 하는 경우 또 다른 용도가 될 것입니다.

가능한 사용 사례



근처에 연결된 장치에서 메시지를 보내거나 가져옵니다.
많은 사용 사례에 적용할 수 있습니다.

택시 차량

택시 함대에서 새로운 승객이 여행을 요청한다고 가정하고,
그들은 우리에게 픽업 지점을 제공하고 시스템은 가장 가까운 택시로의 여행을 제공해야 합니다. 그런 다음 주변 지역의 택시에만 방송하면 됩니다.

센서 메쉬

센서 메시에서 장치 중 하나가 비정상적인 강의 값을 읽고 주어진 반경 내 주변 장치의 값을 알고 싶어합니다.

배달 음식

배송업체 오토바이 한 대가 타이어가 부러져
그런 다음 가까운 파트너에게 도움을 요청하여 음식을 분배하십시오.

목차


  • Dependencies
  • The application
  • Space Zonification
  • Filtering by distance
  • Devices Registry
  • Socket Handler

  • 종속성

    Showing the dependencies is the classic way to start an Elixir post.

      defp deps do
        [
          {:cowboy, "~> 2.4"},
          {:plug_cowboy, "~> 2.0"},
          {:jason, "~> 1.3"},
          {:geocoder, "~> 1.1"},
          {:geocalc, "~> 0.8"}
        ]
      end
    

    응용 프로그램

    The application is the core where all the involved processes will be started.

    defmodule IotDevicesApp do
      use Application
      def start(_type, _args) do
    
        children = [
          Plug.Cowboy.child_spec(
            scheme: :http,
            plug:  SocketApp.HttpRouter,
            options: [dispatch: dispatch(),port: 5000]
          ),
          Registry.child_spec(
            keys: :duplicate,
            name: Registry.IotApp
          )
        ]
    
        opts = [strategy: :one_for_one, name: IotDevicesApp.Application]
        Supervisor.start_link(children, opts)
      end
    
      defp dispatch do
        [
          {:_,
            [
              {"/ws/[...]", IotDevicesApp.SocketHandlerLocation, []},
              {:_, Plug.Cowboy.Handler, {SocketApp.HttpRouter, []}}
            ]
          }
        ]
      end  
    end
    
    

    공간 구역화

    In our app we have thousand of devices spread along a State, County or City, so measure the distance with all the device is not performant, a coarse zonification is nedeed and then apply a fine graded filtering using distance.

    First solution that come to mind is a zonificate by city, using a reverse geocoding service like
    Google Maps
    또는 OpenStreetMap

    지오코더 라이브러리는 우리의 필요에 맞습니다.
    Hexdocs
    Github

    def getZone({lon, lat}) do 
      {:ok, coord} = Geocoder.call({lat, lon}, provider: Geocoder.Providers.OpenStreetMaps)
      # OpenStreetMaps is only for testing, it is limited to 1 request by second.
      "#{coord.location.state}, #{coord.location.city}"
    end
    
    


    기능 테스트:

    
    times_square = {-73.985130,40.758896}
    getZone(times_square)
    Return : "New York, New York"
    
    


    거리로 필터링

    A distance function must to be implemented, some question are nedeed:

    • How far away are the neighbors ? 1km, 5km, 10km
    • Which level of accuracy we need 1m, 100m, 500m
    • Need for speed ?
    Two options are presented:
    A) The fast way work in flat earth calculating the "Euclidean distance" in degrees,
    1 degrees at equator 69 miles

    def distance({cx, cy}, {nx, ny}) do
      :math.sqrt(:math.pow(abs(cx - nx), 2) + :math.pow(abs(cy - ny), 2))
    end
    
    


    B) 많은 미적분학이 관련되어 있기 때문에 정확하고 약간 느릴 수 있습니다. GeoCalc에서 'haversine' 공식을 적용하십시오.

    def distance({cx, cy}, {nx, ny}) do  
      Geocalc.distance_between([cx, cy], [nx, ny])
    end
    
    


    장치 레지스트리

    It is necessary to store an keep a record ot all the connected devices, the right place is a Registry working as publisher subscriber. The PID and the location of every new connection is stored in the Registry, it will be showed bellow, in the Socket Handler.

    Elixir Registry
    레지스트리는 ETS(Erlang Term Storage)를 기반으로 합니다.
    Erlang Term Storage

    소켓 핸들러

    The start point to understand socket handler is the documentation: Cowboy WebSocket

    defmodule IotDevicesApp.SocketHandlerLocation do
      @behaviour :cowboy_websocket
      @degrees  0.5
      @meters   1500
    
      # When a client open the connection, it send to us his latitude and longitude
      # javascript: websocket = new WebSocket("ws://192.168.1.109:5000/ws?lon=-73.985130&lat=40.758896");
      def init(request, _state) do   
    
        vars = URI.query_decoder(request.qs) |> Map.new()    
    
        lon_float = String.to_float(Map.get(vars, "lon", 0.0))
        lat_float = String.to_float(Map.get(vars, "lat", 0.0))
        {:cowboy_websocket, request, {lon_float, lat_float}, %{idle_timeout: :infinity}}
      end
    
    
      # Called once the connection has been upgraded to Websocket.
    
      def websocket_init(location) do
        IO.puts("-------------websocket_init--------------")
        # this function have different PID that the init() function    
        # for that reason the PID registration is here
        key = getZone(location)
        Registry.IotApp
        |> Registry.register(key, location)
    
        {:ok, {key, location}}
      end
    
      # websocket_handle called when:
      # javascript: websocket.send(JSON.stringify({action:"updlocation", lon:11, lat:22})
      # javascript: websocket.send(JSON.stringify({action:"status", ...status})
      # javascript: websocket.send(JSON.stringify({action:"wakeupneighbors"})
    
      def websocket_handle({:text, json}, state = {key, location}) do
        IO.puts("-------------websocket_handle--------------")
    
        IO.inspect(state, label: "state")
    
        payload = Jason.decode!(json)
    
        case payload["action"] do
          "status" -> {:reply, reportStatus(payload["status"]), state}
          "updlocation" -> updateLocation(self(), key, {payload["lon"], payload["lat"]})
          "wakeupneighbors" -> {:reply, wakeupNeighbors(key, location), state}
          _ -> {:ok, state}
        end
      end
    
    
      # Here broadcast happen
      # websocket_info: Will be called for every Erlang message received.
      # Trigger from  wakeupNeighbors()
      def websocket_info(info, state) do
        {:reply, {:text, info}, state}
      end
    
      #----------- Next are utilities functions ---------------------
    
      def reportStatus(status) do
        IO.puts("-------------reportStatus--------------")
        IO.inspect(status ,label: "status")
        # Here put your logic
        {:ok, str} = Jason.encode(%{"action"=>"ack"})
        {:text, str}
      end
    
      def updateLocation(pid, oldkey, newlocation) do
        IO.puts("-------------updateLocation--------------")
        IO.inspect({pid, self()}, label: "PID")
        # this function have same PID that the websocket_init() function
        # IO.inspect(newlocation ,label: "newlocation")
        newkey = getZone(newlocation)
    
        IO.inspect(Registry.values(Registry.IotApp, oldkey, self()), label: "old location")
        Registry.unregister(Registry.IotApp, oldkey)
        Registry.register(Registry.IotApp, newkey, newlocation)
        IO.inspect(Registry.values(Registry.IotApp, newkey, self()), label: "new location")
        {:ok, str} = Jason.encode(%{"action" => "ack"})
        {:reply, {:text, str}, {newkey, newlocation}}
      end
    
    
      def wakeupNeighbors(key, location) do
        # this function have same PID that the websocket_init() function
        IO.puts("-------------wakeupNeighbors--------------")
        {:ok, message} = Jason.encode(%{"action" => "report_status"})
    
        Registry.IotApp
        |> Registry.dispatch(key, fn entries ->
          for {pid, neighbor} <- entries do
            IO.inspect(neighbor, label: "neighbor location")
    
            if pid != self() && isNeighbour(location, neighbor) do
              IO.inspect(pid, label: "      send msg to")
              Process.send(pid, message, [])
            end
          end
        end)
    
        {:ok, str} = Jason.encode(%{"action" => "ack"})
        {:text, str}
      end
    
      def isNeighbour(location, neighbor) do
        # @degrees > distanceEuclidean(location,neighbor)
        @meters > distanceHaversine(location, neighbor)
      end
    
      def distanceEuclidean({cx, cy}, {nx, ny}) do
        :math.sqrt(:math.pow(abs(cx - nx), 2) + :math.pow(abs(cy - ny), 2))
      end
    
      def distanceHaversine({cx, cy}, {nx, ny}) do
        Geocalc.distance_between([cx, cy], [nx, ny])
      end
    
      def getZone({lon, lat}) do
        {:ok, coord} = Geocoder.call({lat, lon}, provider: Geocoder.Providers.OpenStreetMaps)
    
        # OpenStreetMaps is only for testing, it is limited to 1 request by second.
    
        key = "#{coord.location.state}, #{coord.location.city}"
        IO.inspect(key, label: "getZone")
        key
      end
    
    end
    
    
    

    좋은 웹페이지 즐겨찾기