├── .tool-versions ├── Procfile ├── phoenix_static_buildpack.config ├── lib ├── cargo_elixir_web │ ├── templates │ │ ├── page │ │ │ └── index.html.eex │ │ └── layout │ │ │ └── app.html.eex │ ├── views │ │ ├── page_view.ex │ │ ├── layout_view.ex │ │ ├── error_view.ex │ │ └── error_helpers.ex │ ├── channels │ │ ├── payload_channel.ex │ │ └── user_socket.ex │ ├── controllers │ │ ├── user_controller.ex │ │ ├── page_controller.ex │ │ └── payload_controller.ex │ ├── gettext.ex │ ├── router.ex │ └── endpoint.ex ├── cargo_elixir │ ├── repo.ex │ ├── users │ │ ├── users.ex │ │ └── user.ex │ ├── release.ex │ ├── application.ex │ └── payloads │ │ ├── payload.ex │ │ └── payloads.ex ├── mix │ └── tasks │ │ └── refresh_matviews.ex ├── cargo_elixir.ex └── cargo_elixir_web.ex ├── priv ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20200303181332_update_payload_table.exs │ │ ├── 20191209210301_add_snr_and_fingerprint.exs │ │ ├── 20191211210339_create_user_table.exs │ │ └── 20191126185915_create_payloads.exs │ ├── seeds.exs │ └── materialized_view.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── test ├── test_helper.exs ├── cargo_elixir_web │ ├── views │ │ ├── page_view_test.exs │ │ ├── layout_view_test.exs │ │ └── error_view_test.exs │ └── controllers │ │ └── page_controller_test.exs └── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex ├── assets ├── static │ ├── favicon.ico │ ├── images │ │ ├── phoenix.png │ │ ├── logocargo.svg │ │ └── logocargo_30.svg │ └── robots.txt ├── js │ ├── data │ │ ├── Rest.js │ │ └── chart.js │ ├── app.js │ ├── components │ │ ├── SearchBar.js │ │ ├── NavBarRow.js │ │ ├── SignUp.js │ │ ├── Timeline.js │ │ ├── NavBar.js │ │ └── Inspector.js │ ├── pages │ │ ├── StatsPage.js │ │ ├── mapStyle.js │ │ └── MapScreen.js │ └── socket.js ├── .babelrc ├── css │ └── app.css ├── webpack.config.js └── package.json ├── elixir_buildpack.config ├── .formatter.exs ├── entrypoint.sh ├── config ├── test.exs ├── releases.exs ├── config.exs ├── prod.secret.exs ├── dev.exs └── prod.exs ├── docker-compose.yaml ├── Dockerfile ├── CONTRIBUTING.md ├── .gitignore ├── README.md ├── mix.exs ├── LICENSE └── mix.lock /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.9.0-otp-21 2 | erlang 21.1 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: MIX_ENV=prod POOL_SIZE=16 mix phx.server 2 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | node_version=16.20.0 2 | phoenix_ex=phx 3 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(CargoElixir.Repo, :manual) 3 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/cargo-elixir/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/cargo-elixir/HEAD/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | # Erlang version 2 | erlang_version=24.3.4.14 3 | 4 | # Elixir version 5 | elixir_version=1.12 6 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.PageView do 2 | use CargoElixirWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.LayoutView do 2 | use CargoElixirWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/cargo_elixir_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.PageViewTest do 2 | use CargoElixirWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/cargo_elixir_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.LayoutViewTest do 2 | use CargoElixirWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /lib/cargo_elixir/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir.Repo do 2 | use Ecto.Repo, 3 | otp_app: :cargo_elixir, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /assets/js/data/Rest.js: -------------------------------------------------------------------------------- 1 | 2 | const DOMAIN = '/api/' 3 | // const DOMAIN = 'https://cargo.helium.com/api/' 4 | 5 | export const get = (url) => fetch(DOMAIN + url) 6 | -------------------------------------------------------------------------------- /lib/mix/tasks/refresh_matviews.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.RefreshMatviews do 2 | use Mix.Task 3 | 4 | def run(db) do 5 | Mix.shell.cmd("psql -a #{db} -c 'select refresh_all_matviews()'") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | subdirectories: ["priv/*/migrations"] 6 | ] 7 | -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/cargo_elixir_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.PageControllerTest do 2 | use CargoElixirWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/channels/payload_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.PayloadChannel do 2 | use Phoenix.Channel 3 | 4 | def join("payload:new", _msg, socket) do 5 | {:ok, socket} 6 | end 7 | end 8 | 9 | # CargoElixirWeb.Endpoint.broadcast! "payload:new", "new_payload", %{data: 1} 10 | -------------------------------------------------------------------------------- /lib/cargo_elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir do 2 | @moduledoc """ 3 | CargoElixir keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/cargo_elixir/users/users.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir.Users do 2 | import Ecto.Query, warn: false 3 | alias CargoElixir.Repo 4 | 5 | alias CargoElixir.Users.User 6 | 7 | def create_user(user_params) do 8 | %User{} 9 | |> User.changeset(user_params) 10 | |> Repo.insert() 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20200303181332_update_payload_table.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir.Repo.Migrations.UpdatePayloadTablew do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("payloads") do 6 | add :name, :string 7 | modify :device_id, :string, null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.UserController do 2 | use CargoElixirWeb, :controller 3 | 4 | alias CargoElixir.Users 5 | alias CargoElixir.Users.User 6 | 7 | def create(conn, params) do 8 | with {:ok, %User{}} <- Users.create_user(params) do 9 | conn |> send_resp(201, "") 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20191209210301_add_snr_and_fingerprint.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir.Repo.Migrations.AddSnrAndFingerprint do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:payloads, primary_key: false) do 6 | add :fingerprint, :string, default: "none", null: false 7 | add :snr, :decimal, default: 0, null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # CargoElixir.Repo.insert!(%CargoElixir.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | ">0.25%", 9 | "not ie 11", 10 | "not op_mini all" 11 | ] 12 | } 13 | } 14 | ], 15 | "@babel/preset-react" 16 | ], 17 | "plugins": [ 18 | "@babel/plugin-transform-runtime" 19 | ] 20 | } -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | # File: my_app/entrypoint.sh 2 | #!/bin/bash 3 | # docker entrypoint script. 4 | 5 | # assign a default for the database_user 6 | DB_USER=${DATABASE_USER:-postgres} 7 | 8 | # wait until Postgres is ready 9 | while ! pg_isready -q -h $DATABASE_HOST -p 5432 -U $DB_USER 10 | do 11 | echo "$(date) - waiting for database to start" 12 | sleep 2 13 | done 14 | 15 | bin="_build/prod/rel/cargo_elixir/bin/cargo_elixir" 16 | eval "$bin eval \"CargoElixir.Release.migrate\"" 17 | exec "$bin" "start" 18 | -------------------------------------------------------------------------------- /test/cargo_elixir_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.ErrorViewTest do 2 | use CargoElixirWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(CargoElixirWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(CargoElixirWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.PageController do 2 | use CargoElixirWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | 8 | def get_console_stats(conn, _params) do 9 | case HTTPoison.get("https://console.helium.com/api/stats", ["Authorization": Application.fetch_env!(:cargo_elixir, :console_stats_secret)]) do 10 | {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> 11 | conn |> json(Jason.decode!(body)) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :cargo_elixir, CargoElixir.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "cargo_elixir_test", 8 | hostname: "localhost", 9 | pool: Ecto.Adapters.SQL.Sandbox 10 | 11 | # We don't run a server during test. If one is required, 12 | # you can enable the server option below. 13 | config :cargo_elixir, CargoElixirWeb.Endpoint, 14 | http: [port: 4002], 15 | server: false 16 | 17 | # Print only warnings and errors during test 18 | config :logger, level: :warn 19 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | cargo: 4 | build: . 5 | image: helium/cargo:latest 6 | container_name: helium_cargo 7 | environment: 8 | - SECRET_KEY_BASE=${SECRET_KEY_BASE} 9 | - DATABASE_DB=cargo_elixir 10 | - DATABASE_HOST=postgres 11 | ports: 12 | - "4000:4000" 13 | depends_on: 14 | - postgres 15 | postgres: 16 | image: postgres 17 | container_name: helium_postgres 18 | environment: 19 | - POSTGRES_DB=cargo_elixir 20 | - POSTGRES_PASSWORD=postgres 21 | restart: always 22 | -------------------------------------------------------------------------------- /lib/cargo_elixir_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixirWeb.ErrorView do 2 | use CargoElixirWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/cargo_elixir/release.ex: -------------------------------------------------------------------------------- 1 | defmodule CargoElixir.Release do 2 | @app :cargo_elixir 3 | 4 | def migrate do 5 | load_app() 6 | 7 | for repo <- repos() do 8 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 9 | end 10 | end 11 | 12 | def rollback(repo, version) do 13 | load_app() 14 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 15 | end 16 | 17 | defp repos do 18 | Application.fetch_env!(@app, :ecto_repos) 19 | end 20 | 21 | defp load_app do 22 | Application.load(@app) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import '../css/app.css' 4 | import { BrowserRouter as Router, Route, Link } from 'react-router-dom' 5 | import MapScreen from "./pages/MapScreen" 6 | import StatsPage from "./pages/StatsPage" 7 | 8 | class App extends React.Component { 9 | render() { 10 | return ( 11 |
4 |
5 | Cargo is a visualization tool for asset trackers. As part of our strategy we are making this application available as open source.
6 |
7 | Helium Console has a pre-built Console integration that makes it easy to quickly view a tracker’s physical location.
8 |
9 | ## Development Environment
10 |
11 | To start your Phoenix server:
12 |
13 | * Install dependencies with `mix deps.get`
14 | * Create and migrate your database with `mix ecto.setup`
15 | * Install Node.js dependencies with `cd assets && yarn`
16 | * Start Phoenix endpoint with `mix phx.server`
17 |
18 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
19 |
20 | ## Running with Docker
21 |
22 | * Clone the repo and `cd cargo-elixir`
23 | * Follow instructions at the bottom of `/config/prod.exs`
24 | * Follow instructions at the top of `/assets/js/pages/MapScreen.js`
25 | * Update host in `/config/releases.exs`
26 | * Build with `docker-compose build`
27 | * Generate secret key with `mix phx.gen.secret`
28 | * Set environment variable `export SECRET_KEY_BASE=Currently transmitting devices: {!stats ? "loading" : stats.currentlyTransmitting}
43 |Devices that transmitted ({tabs[tabIndex].title}): {!stats ? "loading" : stats.devicesTransmitted}
44 |Hotspots that transmitted ({tabs[tabIndex].title}): {!stats ? "loading" : stats.hotspotsTransmitted}
45 |Packets transmitted ({tabs[tabIndex].title}): {!stats ? "loading" : stats.payloadsTransmitted}
46 | { 47 | tabs.map((t, i) => ( 48 | 49 | )) 50 | } 51 |Number of Users: {consoleStats.users}
53 |Number of Organizations: {consoleStats.organizations}
54 |Number of Teams: {consoleStats.teams}
55 |Number of Devices: {consoleStats.devices}
56 |{device.name}
85 |{timeAgo.format(latest, {flavour: "small"})}
90 | ) : ( 91 |No GPS Fix
92 | ) 93 | ) : ( 94 |{timeAgo.format(latest, {flavour: "small"})}
95 | ) 96 | } 97 |{name}
98 |Enter your details to discuss IoT asset tracking use cases with Helium.
139 | 161 |167 | {value.split("-")[0]} 168 |
169 |seq #
170 |178 | {value} 179 |
180 |mph
181 |189 | {value} 190 |
191 |meters
192 |200 | {value} 201 |
202 |dBm
203 |211 | {value} 212 |
213 |volts
214 |222 | {value} 223 |
224 |SNR
225 |Devices
151 |Show Hotspot Paths
156 |{hotspots.data.length} Hotspots Witnessed
158 |highlightHotspot(h)}>{h.name}
164 | ) 165 | } 166 | return ( 167 |highlightHotspot(h)}>{h.name}
168 | ) 169 | }) 170 | } 171 |Hotspots List
183 | { 184 | showHS ? : 185 | } 186 |Last Packet Stats
208 | { 209 | show ? : 210 | } 211 |Seq. No.
218 |{lastPacket.seq_id.split("-")[0]}
219 |Avg Speed:
222 |{lastPacket.speed}mph
223 |Elevation
227 |{lastPacket.elevation.toFixed(0)}m
228 |Voltage
231 |{lastPacket.battery.toFixed(2)}v
232 |RSSI
236 |{lastPacket.rssi}dBm
237 |SNR
240 |n/a
241 |Last Packet Stats
254 | { 255 | show ? : 256 | } 257 |Name:
263 |{selectedDevice.name}
264 |Hotspot:
265 |{startCase(lastPacket.hotspots[lastPacket.hotspots.length - 1])}
266 |Sequence No.
270 |{lastPacket.seq_id.split("-")[0]}
271 |Avg Speed
274 |{lastPacket.speed}mph
275 |Elevation
279 |{lastPacket.elevation.toFixed(0)}m
280 |Voltage
283 |{lastPacket.battery.toFixed(2)}v
284 |RSSI
288 |{lastPacket.rssi}dBm
289 |SNR
292 |{(Math.round(lastPacket.snr * 100) / 100).toFixed(2)}
293 |