├── .dockerignore ├── .env-template ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── build.yml │ ├── ci_build.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile-CI ├── Dockerfile-docs ├── LICENSE ├── Makefile ├── README.md ├── c_src └── compile.sh ├── config ├── grpc_gen.config ├── localhost ├── sys.config ├── sys.config.src ├── test.config ├── testnet.config.src └── vm.args ├── docker-compose-local.yaml ├── docker-compose.yaml ├── docs ├── Device.md ├── Organizations.md ├── README.md ├── XOR_Filter.md ├── decoder.plantuml ├── decoder.png ├── decoder.svg ├── erlang-processes.plantuml ├── erlang-processes.png ├── erlang-processes.svg └── runbook.md ├── grafana-dashboard.json ├── include ├── lorawan.hrl ├── lorawan_adr.hrl ├── lorawan_db.hrl ├── lorawan_vars.hrl ├── metrics.hrl ├── router_device.hrl └── router_device_worker.hrl ├── priv ├── genesis.mainnet └── genesis.testnet ├── prometheus-template.yml ├── rebar.config ├── rebar.lock ├── rebar3 ├── scripts ├── extensions │ ├── device │ ├── filter │ ├── info │ ├── migration │ └── organization ├── monitor_blocks.sh └── save_logs.sh ├── src ├── apis │ ├── router_console_api.erl │ ├── router_console_dc_tracker.erl │ ├── router_console_sup.erl │ ├── router_console_ws_handler.erl │ └── router_console_ws_worker.erl ├── channels │ ├── router_aws_channel.erl │ ├── router_channel.erl │ ├── router_channel_utils.erl │ ├── router_console_channel.erl │ ├── router_http_channel.erl │ ├── router_iot_central_channel.erl │ ├── router_iot_central_connection.erl │ ├── router_iot_hub_channel.erl │ ├── router_iot_hub_connection.erl │ ├── router_mqtt_channel.erl │ └── router_no_channel.erl ├── cli │ ├── router_cli_device_worker.erl │ ├── router_cli_info.erl │ ├── router_cli_migration.erl │ ├── router_cli_organization.erl │ ├── router_cli_registry.erl │ ├── router_cli_xor_filter.erl │ └── router_console.erl ├── decoders │ ├── router_decoder.erl │ ├── router_decoder_browan_object_locator.erl │ ├── router_decoder_cayenne.erl │ ├── router_decoder_custom_sup.erl │ ├── router_decoder_custom_worker.erl │ ├── router_decoder_sup.erl │ └── router_v8.erl ├── device │ ├── router_device.erl │ ├── router_device_cache.erl │ ├── router_device_channels_worker.erl │ ├── router_device_devaddr.erl │ ├── router_device_multibuy.erl │ ├── router_device_routing.erl │ ├── router_device_stats.erl │ ├── router_device_worker.erl │ └── router_devices_sup.erl ├── grpc │ ├── helium_packet_service.erl │ ├── helium_router_service.erl │ ├── router_cli_migration_skf_list_handler.erl │ ├── router_grpc_client_worker.erl │ ├── router_grpc_server_worker.erl │ ├── router_ics_eui_worker.erl │ ├── router_ics_gateway_location_worker.erl │ ├── router_ics_route_get_devaddrs_handler.erl │ ├── router_ics_route_get_euis_handler.erl │ ├── router_ics_skf_list_handler.erl │ ├── router_ics_skf_worker.erl │ ├── router_ics_utils.erl │ └── router_skf_reconcile.erl ├── lora │ ├── lorawan_mac_commands.erl │ ├── lorawan_rxdelay.erl │ └── lorawan_utils.erl ├── metrics │ ├── router_metrics.erl │ └── router_metrics_reporter.erl ├── router.app.src ├── router_app.erl ├── router_blockchain.erl ├── router_db.erl ├── router_discovery.erl ├── router_discovery_handler.erl ├── router_handler.erl ├── router_sc_worker.erl ├── router_sup.erl ├── router_utils.erl └── router_xor_filter_worker.erl └── test ├── blockchain_test_utils.erl ├── console_callback.erl ├── console_test.hrl ├── router_SUITE.erl ├── router_channel_aws_SUITE.erl ├── router_channel_console_SUITE.erl ├── router_channel_http_SUITE.erl ├── router_channel_iot_central_SUITE.erl ├── router_channel_iot_hub_SUITE.erl ├── router_channel_mqtt_SUITE.erl ├── router_channel_no_channel_SUITE.erl ├── router_console_api_SUITE.erl ├── router_console_dc_tracker_SUITE.erl ├── router_ct_macros.hrl ├── router_ct_utils.erl ├── router_data_SUITE.erl ├── router_decoder_SUITE.erl ├── router_decoder_custom_sup_SUITE.erl ├── router_decoder_custom_worker_SUITE.erl ├── router_device_channels_worker_SUITE.erl ├── router_device_devaddr_SUITE.erl ├── router_device_routing_SUITE.erl ├── router_device_worker_SUITE.erl ├── router_discovery_SUITE.erl ├── router_discovery_handler_test.erl ├── router_downlink_SUITE.erl ├── router_grpc_SUITE.erl ├── router_handler_test.erl ├── router_ics_eui_worker_SUITE.erl ├── router_ics_gateway_location_worker_SUITE.erl ├── router_ics_skf_worker_SUITE.erl ├── router_lorawan_SUITE.erl ├── router_lorawan_handler_test.erl ├── router_metrics_SUITE.erl ├── router_sc_worker_SUITE.erl ├── router_test_gateway.erl ├── router_test_ics_gateway_service.erl ├── router_test_ics_route_service.erl ├── router_v8_SUITE.erl ├── router_xor_filter_SUITE.erl └── test_utils.erl /.dockerignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | log/ 3 | data/ -------------------------------------------------------------------------------- /.env-template: -------------------------------------------------------------------------------- 1 | # OUI used by router (see https://developer.helium.com/blockchain/blockchain-cli#oui) 2 | ROUTER_OUI=999 3 | 4 | # Set Routers NAT info 5 | ROUTER_NAT_INTERNAL_IP=XX.XXX.XX.XXX 6 | ROUTER_NAT_INTERNAL_PORT=2154 7 | ROUTER_NAT_EXTERNAL_IP=XX.XXX.XX.XXX 8 | ROUTER_NAT_EXTERNAL_PORT=2154 9 | 10 | # STATE CHANNELS 11 | # FYI: There is a cost to open a state channel (about 35k DC) 12 | # Current min: 15 blocks, max abbout 5000 blocks. Default: 25 blocks when not set 13 | ROUTER_SC_EXPIRATION_INTERVAL=25 14 | # Minimum distance state channels can expire within another expiration 15 | # default: 15 when not set 16 | ROUTER_SC_EXPIRATION_BUFFER=15 17 | # Data credit use to fund a state channel. (note that the amount will be double for overage) 18 | # default: 100 when not set 19 | ROUTER_SC_OPEN_DC_AMOUNT=100 20 | 21 | # CONSOLE 22 | # Console's connection info (see https://github.com/helium/console) 23 | ROUTER_CONSOLE_ENDPOINT=http://helium_console:4000 24 | ROUTER_CONSOLE_WS_ENDPOINT=ws://helium_console:4000/socket/router/websocket 25 | ROUTER_CONSOLE_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 26 | # Public facing endpoint to POST downlinks for devices 27 | ROUTER_CONSOLE_DOWNLINK_ENDPOINT=http://helium_console:4000 28 | 29 | # Turn on/off xor filter worker (anything else than true is off, disabled by default) 30 | # This allows to publish app eui / dev eui for join packets 31 | ROUTER_XOR_FILTER_WORKER=false 32 | 33 | # Max time to wait for uplinks in ms 34 | # Default: 200ms blocks when not set 35 | ROUTER_FRAME_TIMEOUT=500 36 | 37 | # Max time to wait for uplinks in ms, only for disco mode device 38 | # Default: 8 seconds blocks when not set 39 | ROUTER_DISCO_FRAME_TIMEOUT=8000 40 | 41 | # Turn on/off HTTP integration url checks like DNS, IP... (anything else than false is on, enabled by default) 42 | ROUTER_HTTP_CHANNEL_URL_CHECK=true 43 | 44 | # Set max number for downlink allowed to be queued for a device (Defaults to 20). 45 | ROUTER_DEVICE_QUEUE_SIZE_LIMIT=20 46 | 47 | # Charge organization when no offer is found (Default to: true) 48 | ROUTER_CHARGE_WHEN_NO_OFFER=true 49 | 50 | # Set resolution at which device addresses are alocated (Default to 3) 51 | # Ex: resolution 3 holds 41162 indexes and your Router has an OUI with 8 device addresses 52 | # It means that it can support up to 329296 devices before seeing some potential conflicts. 53 | # Note: 54 | # - Conflicts are still possible but less likely. 55 | # - Resolution 3 or 4 is recommended as they will provide big enough hexagons to avoid conflicts. 56 | # See https://h3geo.org/docs/core-library/restable/ for resolution data 57 | ROUTER_DEVADDR_ALLOCATE_RESOLUTION=3 58 | 59 | # Set http port for Prometheus handler to come scrape metrics (Default to port 3000) 60 | ROUTER_METRICS_PORT=3000 61 | 62 | # The first 7 bits of a DevAddr as an integer. (default: 72 :: $H) 63 | # __DISCLAIMER__: 64 | # This setting _only_ works for NetIDs of Type-0. 65 | # The integer represents the first 7-bits of a DevAddr. NOT the Network ID. 66 | # Change at your own discretion, this may lead to instability in assigning DevAddrs to devices. 67 | ROUTER_DEVADDR_PREFIX=72 68 | 69 | # Charge an Organization for join requests (default: true) 70 | ROUTER_CHARGE_JOINS=true 71 | 72 | # Charge an org for a packet received late or replayed (default: false). 73 | ROUTER_CHARGE_LATE_PACKETS=false 74 | 75 | # Flag to disable preferred hotspot feature (default: false). 76 | ROUTER_DISABLE_PREFERRED_HOTSPOT=true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.erl text eol=lf 4 | *.hrl text eol=lf 5 | *.c text eol=lf 6 | *.cpp text eol=lf 7 | *.h text eol=lf 8 | *.md text eol=lf 9 | *.org text eol=lf 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: macpie 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Device Info (please complete the following information):** 20 | - Environment (Production, Staging) 21 | - Console Device ID 22 | - Device dev eui / app eui 23 | - How often is device sending data 24 | - Region 25 | - Hotspots (names) used to transfer data 26 | 27 | *NOTE: do not post your device `app_key`* 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | 32 | **Screenshots** 33 | If applicable, add screenshots (from Console) to help explain your problem. 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: '*' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Build and push Docker images 15 | uses: docker/build-push-action@v1 16 | with: 17 | registry: quay.io 18 | username: ${{ secrets.QUAY_USERNAME }} 19 | password: ${{ secrets.QUAY_PASSWORD }} 20 | repository: team-helium/router 21 | tag_with_ref: true 22 | push: true 23 | build_args: ROUTER_VERSION=${{ github.ref_name }} -------------------------------------------------------------------------------- /.github/workflows/ci_build.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | push: 5 | branches: master 6 | workflow_dispatch: 7 | 8 | jobs: 9 | CI_build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cancel previous runs 13 | uses: styfle/cancel-workflow-action@0.9.1 14 | with: 15 | access_token: ${{ github.token }} 16 | - name: checkout 17 | uses: actions/checkout@v2 18 | - name: Build and push Docker images 19 | uses: docker/build-push-action@v1 20 | with: 21 | registry: quay.io 22 | username: ${{ secrets.QUAY_USERNAME }} 23 | password: ${{ secrets.QUAY_PASSWORD }} 24 | repository: team-helium/router 25 | tags: CI 26 | file: Dockerfile-CI 27 | push: true 28 | build_args: ROUTER_VERSION=${{ github.ref_name }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: master 8 | # Run every day at midnight PST (0800 UTC) 9 | # https://crontab.guru/#0_8_*_*_* 10 | schedule: 11 | - cron: '0 8 * * *' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | container: 18 | image: quay.io/team-helium/router:CI 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | steps: 23 | # For some reason the toolchain is no installed properly so lets add it... 24 | - name: rustup default stable 25 | run: rustup default stable 26 | - name: checkout 27 | uses: actions/checkout@v2 28 | - name: cleanup 29 | run: | 30 | mkdir -p /tmp/router/_build 31 | cp -R /opt/router/_build/* /tmp/router/_build 32 | cp -R * /tmp/router 33 | rm -rf /opt/router/* 34 | cp -R /tmp/router /opt 35 | rm -rf /tmp/router 36 | - name: build 37 | run: | 38 | make grpc 39 | ./rebar3 compile 40 | working-directory: /opt/router 41 | - name: build test 42 | run: ./rebar3 as test compile 43 | working-directory: /opt/router 44 | - name: tar 45 | run: tar -cvzf build.tar.gz -C _build/ . 46 | working-directory: /opt/router 47 | - name: upload-artifact 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: build 51 | path: /opt/router/build.tar.gz 52 | xref: 53 | needs: build 54 | runs-on: ubuntu-latest 55 | container: 56 | image: quay.io/team-helium/router:CI 57 | concurrency: 58 | group: ${{ github.workflow }}-${{ github.ref }}-xref 59 | cancel-in-progress: true 60 | steps: 61 | - name: checkout 62 | uses: actions/checkout@v2 63 | - name: download-artifact 64 | uses: actions/download-artifact@v2 65 | with: 66 | name: build 67 | - name: untar 68 | run: | 69 | mkdir _build 70 | tar -xvf build.tar.gz -C _build 71 | - name: grpc 72 | run: make grpc 73 | - name: xref 74 | run: make xref 75 | eunit: 76 | needs: build 77 | runs-on: ubuntu-latest 78 | container: 79 | image: quay.io/team-helium/router:CI 80 | concurrency: 81 | group: ${{ github.workflow }}-${{ github.ref }}-eunit 82 | cancel-in-progress: true 83 | steps: 84 | - name: checkout 85 | uses: actions/checkout@v2 86 | - name: download-artifact 87 | uses: actions/download-artifact@v2 88 | with: 89 | name: build 90 | - name: untar 91 | run: | 92 | mkdir _build 93 | tar -xvf build.tar.gz -C _build 94 | - name: grpc 95 | run: make grpc 96 | - name: eunit 97 | run: make eunit -v 98 | dialyzer: 99 | needs: build 100 | runs-on: ubuntu-latest 101 | container: 102 | image: quay.io/team-helium/router:CI 103 | concurrency: 104 | group: ${{ github.workflow }}-${{ github.ref }}-dialyzer 105 | cancel-in-progress: true 106 | steps: 107 | - name: checkout 108 | uses: actions/checkout@v2 109 | - name: download-artifact 110 | uses: actions/download-artifact@v2 111 | with: 112 | name: build 113 | - name: untar 114 | run: | 115 | mkdir _build 116 | tar -xvf build.tar.gz -C _build 117 | - name: grpc 118 | run: make grpc 119 | - name: dialyzer 120 | run: make dialyzer 121 | suites: 122 | runs-on: ubuntu-latest 123 | outputs: 124 | test_suites: ${{ steps.gen_test_suites.outputs.test_suites }} 125 | steps: 126 | - name: checkout 127 | uses: actions/checkout@v2 128 | - name: gen_test_suites 129 | id: gen_test_suites 130 | run: | 131 | SUITES="[" 132 | for F in ./test/*_SUITE.erl 133 | do 134 | SUITES+="\"$(basename $F)\","; 135 | done 136 | # remove last comma 137 | SUITES=${SUITES%,}; 138 | # close array 139 | SUITES+="]" 140 | echo "We are in $(pwd)" 141 | echo "::set-output name=test_suites::${SUITES}" 142 | ct: 143 | needs: [build, suites] 144 | runs-on: ubuntu-latest 145 | container: 146 | image: quay.io/team-helium/router:CI 147 | strategy: 148 | fail-fast: false 149 | matrix: 150 | suite: "${{ fromJSON(needs.suites.outputs.test_suites) }}" 151 | concurrency: 152 | group: ${{ github.workflow }}-${{ github.ref }}-ct-${{ matrix.suite }} 153 | cancel-in-progress: true 154 | env: 155 | IC_APP_NAME: '${{ secrets.IC_APP_NAME }}' 156 | IC_SCOPE_ID: '${{ secrets.IC_SCOPE_ID }}' 157 | IC_API_KEY: '${{ secrets.IC_API_KEY }}' 158 | steps: 159 | - name: checkout 160 | uses: actions/checkout@v2 161 | - name: download-artifact 162 | uses: actions/download-artifact@v2 163 | with: 164 | name: build 165 | - name: untar 166 | run: | 167 | mkdir _build 168 | tar -xvf build.tar.gz -C _build 169 | - name: ct ${{ matrix.suite }} 170 | run: | 171 | make grpc 172 | CT_LAGER=DEBUG ./rebar3 ct --suite=${{ matrix.suite }} --readable=true 173 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | log/ 20 | data/ 21 | src/pb/ 22 | src/grpc/autogen 23 | .env 24 | prometheus.yml 25 | doc/ 26 | .DS_Store 27 | *.coverdata -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### How to Contribute to this repository 2 | 3 | We value contributions from the community and will do everything we 4 | can go get them reviewed in a timely fashion. If you have code to send 5 | our way or a bug to report: 6 | 7 | * **Contributing Code**: If you have new code or a bug fix, fork this 8 | repo, create a logically-named branch, and [submit a PR against this 9 | repo](https://github.com/helium/router). Include a 10 | write up of the PR with details on what it does. 11 | 12 | * **Reporting Bugs**: Open an issue [against this 13 | repo](https://github.com/helium/router/issues) with as 14 | much detail as you can. At the very least you'll include steps to 15 | reproduce the problem. 16 | 17 | This project is intended to be a safe, welcoming space for 18 | collaboration, and contributors are expected to adhere to the 19 | [Contributor Covenant Code of 20 | Conduct](http://contributor-covenant.org/). 21 | 22 | Above all, thank you for taking the time to be a part of the Helium 23 | community. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM erlang:24.3.3 2 | ENV DEBIAN_FRONTEND noninteractive 3 | 4 | RUN apt update 5 | RUN apt-get install -y -q \ 6 | build-essential \ 7 | bison \ 8 | flex \ 9 | git \ 10 | gzip \ 11 | autotools-dev \ 12 | automake \ 13 | libtool \ 14 | pkg-config \ 15 | cmake \ 16 | libsodium-dev \ 17 | iproute2 18 | 19 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable 20 | ENV PATH="/root/.cargo/bin:${PATH}" 21 | RUN rustup update 22 | 23 | WORKDIR /opt/router 24 | 25 | ARG BUILD_NET=mainnet 26 | ENV BUILD_WITHOUT_QUIC=1 27 | 28 | ADD rebar3 rebar3 29 | ADD rebar.config rebar.config 30 | ADD rebar.lock rebar.lock 31 | ADD config/grpc_gen.config config/grpc_gen.config 32 | RUN ./rebar3 get-deps 33 | RUN ./rebar3 as ${BUILD_NET} compile 34 | 35 | ADD Makefile Makefile 36 | ADD c_src/ c_src/ 37 | ADD include/ include/ 38 | ADD src/ src/ 39 | ADD scripts/ scripts/ 40 | RUN make 41 | 42 | ADD config/ config/ 43 | ADD priv/genesis.${BUILD_NET} priv/genesis 44 | RUN ./rebar3 as ${BUILD_NET} release 45 | 46 | ENV PATH=$PATH:_build/${BUILD_NET}/rel/router/bin 47 | RUN ln -s /opt/router/_build/${BUILD_NET}/rel /opt/router/_build/default/rel 48 | RUN ln -s /opt/router/_build/default/rel/router/bin/router /opt/router-exec 49 | 50 | ARG ROUTER_VERSION 51 | ENV ROUTER_VERSION=${ROUTER_VERSION:-unknown} 52 | RUN echo ${ROUTER_VERSION} > router.version && cat router.version 53 | 54 | CMD ["make", "run"] 55 | -------------------------------------------------------------------------------- /Dockerfile-CI: -------------------------------------------------------------------------------- 1 | FROM erlang:24.3.3 2 | ENV DEBIAN_FRONTEND noninteractive 3 | 4 | RUN apt update 5 | RUN apt-get install -y -q \ 6 | build-essential \ 7 | bison \ 8 | flex \ 9 | git \ 10 | gzip \ 11 | autotools-dev \ 12 | automake \ 13 | libtool \ 14 | pkg-config \ 15 | cmake \ 16 | libsodium-dev \ 17 | iproute2 18 | 19 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable 20 | ENV PATH="/root/.cargo/bin:${PATH}" 21 | RUN rustup update 22 | 23 | WORKDIR /opt/router 24 | 25 | ARG BUILD_NET=mainnet 26 | ENV BUILD_WITHOUT_QUIC=1 27 | 28 | ADD rebar3 rebar3 29 | ADD rebar.config rebar.config 30 | ADD rebar.lock rebar.lock 31 | ADD config/grpc_gen.config config/grpc_gen.config 32 | RUN ./rebar3 get-deps 33 | RUN ./rebar3 as ${BUILD_NET} compile 34 | 35 | # EXTRA from main Dockerfile 36 | # This is only to fetch / compile tests dependencies 37 | RUN ./rebar3 as test compile 38 | # END EXTRA 39 | 40 | ADD Makefile Makefile 41 | ADD c_src/ c_src/ 42 | ADD include/ include/ 43 | ADD src/ src/ 44 | ADD scripts/ scripts/ 45 | RUN make 46 | 47 | ADD config/ config/ 48 | ADD priv/genesis.${BUILD_NET} priv/genesis 49 | RUN ./rebar3 as ${BUILD_NET} release 50 | 51 | ENV PATH=$PATH:_build/${BUILD_NET}/rel/router/bin 52 | RUN ln -s /opt/router/_build/${BUILD_NET}/rel /opt/router/_build/default/rel 53 | RUN ln -s /opt/router/_build/default/rel/router/bin/router /opt/router-exec 54 | 55 | # EXTRA from main Dockerfile 56 | ADD test/ test/ 57 | RUN ./rebar3 as test compile 58 | # END EXTRA 59 | 60 | ARG ROUTER_VERSION 61 | ENV ROUTER_VERSION=${ROUTER_VERSION:-unknown} 62 | RUN echo ${ROUTER_VERSION} > router.version && cat router.version 63 | 64 | CMD ["make", "run"] 65 | -------------------------------------------------------------------------------- /Dockerfile-docs: -------------------------------------------------------------------------------- 1 | FROM debian:testing-slim 2 | 3 | WORKDIR /opt/router 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | RUN apt-get update 7 | RUN apt-get install -y --no-install-recommends plantuml make wget 8 | 9 | # Being Debian-based, ensure use of latest released library: 10 | RUN wget http://sourceforge.net/projects/plantuml/files/plantuml.jar/download -O /usr/share/plantuml/plantuml.jar 11 | 12 | CMD ["make", "docs"] 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: compile clean docs test rel run grpc 2 | .PHONY: docker-build docker-docs docker-test docker-run 3 | 4 | grpc_services_directory=src/grpc/autogen 5 | 6 | OS := $(shell uname -s) 7 | 8 | ## https://www.gnu.org/software/make/manual/html_node/Syntax-of-Functions.html#Syntax-of-Functions 9 | ## for example of join on comma 10 | null := 11 | space := $(null) # 12 | comma := , 13 | comma-join-fn = $(subst $(space),$(comma),$(1)) 14 | 15 | ALL_TEST_FILES = $(notdir $(wildcard test/*_SUITE.erl)) 16 | 17 | ifeq ($(OS), Darwin) 18 | LORA_TEST_FILES = $(notdir $(wildcard test/*lorawan*SUITE.erl)) 19 | TEST_SUITES = $(call comma-join-fn,$(filter-out $(LORA_TEST_FILES),$(ALL_TEST_FILES))) 20 | else 21 | TEST_SUITES = $(call comma-join-fn,$(ALL_TEST_FILES)) 22 | endif 23 | 24 | REBAR=./rebar3 25 | 26 | compile: | $(grpc_services_directory) 27 | BUILD_WITHOUT_QUIC=1 $(REBAR) compile 28 | $(REBAR) format 29 | 30 | clean: 31 | git clean -dXfffffffffff 32 | 33 | docs: 34 | @which plantuml || (echo "Run: apt-get install plantuml"; false) 35 | (cd docs/ && plantuml -tsvg *.plantuml) 36 | (cd docs/ && plantuml -tpng *.plantuml) 37 | 38 | test: | $(grpc_services_directory) 39 | $(REBAR) fmt --verbose --check rebar.config 40 | $(REBAR) fmt --verbose --check "{src,include,test}/**/*.{hrl,erl,app.src}" --exclude-files "src/grpc/autogen/**/*" 41 | $(REBAR) fmt --verbose --check "config/{test,sys}.{config,config.src}" 42 | $(REBAR) xref 43 | $(REBAR) eunit 44 | $(REBAR) ct --readable=true --suite=$(TEST_SUITES) 45 | $(REBAR) dialyzer 46 | 47 | rel: | $(grpc_services_directory) 48 | $(REBAR) release 49 | 50 | run: | $(grpc_services_directory) 51 | _build/default/rel/router/bin/router foreground 52 | 53 | docker-build: 54 | docker build -f Dockerfile-CI --force-rm -t quay.io/team-helium/router:local . 55 | 56 | docker-docs: 57 | docker build -f Dockerfile-docs -t router-docs . 58 | docker run --rm -it -v "$(shell pwd):/opt/router" router-docs 59 | 60 | docker-test: 61 | docker run --rm -it --init --name=helium_router_test quay.io/team-helium/router:local make test 62 | 63 | docker-run: 64 | docker run --rm -it --init --env-file=.env --network=host --volume=data:/var/data --name=helium_router quay.io/team-helium/router:local 65 | 66 | docker-exec: 67 | docker exec -it helium_router _build/default/rel/router/bin/router remote_console 68 | 69 | grpc: 70 | REBAR_CONFIG="config/grpc_gen.config" $(REBAR) grpc gen 71 | 72 | $(grpc_services_directory): config/grpc_gen.config 73 | @echo "grpc service directory $(directory) does not exist, generating services" 74 | $(REBAR) get-deps 75 | $(MAKE) grpc 76 | 77 | # Pass all unknown targets straight to rebar3 (e.g. `make dialyzer`) 78 | %: 79 | $(REBAR) $@ 80 | -------------------------------------------------------------------------------- /c_src/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION="e4c02439932dbcdfcd65199a2a361d90ccef99e5" 4 | 5 | 6 | if [ ! -d c_src/LoRaMac-node ]; then 7 | git clone https://github.com/helium/LoRaMac-node.git c_src/LoRaMac-node 8 | fi 9 | 10 | cd c_src/LoRaMac-node 11 | 12 | CURRENT_VERSION=`git rev-parse HEAD` 13 | 14 | if [ ! "$VERSION" = "$CURRENT_VERSION" ]; then 15 | git clean -ddxxff 16 | git fetch 17 | git checkout $VERSION 18 | fi 19 | 20 | for REGION in US915 EU868 AS923 CN470; do 21 | if [ ! -d build_$REGION ]; then 22 | cmake -H. -Bbuild_$REGION -DREGION_$REGION=1 -DACTIVE_REGION=LORAMAC_REGION_$REGION -DAPPLICATION="LoRaMac" -DSUB_PROJECT="classA" -DBOARD="Simul" -DRADIO="radio-simul" 23 | fi 24 | make -C build_$REGION -j 25 | cp build_$REGION/src/apps/LoRaMac/LoRaMac-classA ../../priv/LoRaMac-classA_$REGION 26 | done 27 | -------------------------------------------------------------------------------- /config/grpc_gen.config: -------------------------------------------------------------------------------- 1 | {plugins, [ 2 | {grpcbox_plugin, 3 | {git, "https://github.com/novalabsxyz/grpcbox_plugin.git", 4 | {branch, "andymck/ts-master/combined-opts-and-template-changes"}}} 5 | ]}. 6 | 7 | {grpc, [ 8 | {proto_files, [ 9 | "_build/default/lib/helium_proto/src/service/router.proto", 10 | "_build/default/lib/helium_proto/src/service/packet_router.proto", 11 | "_build/default/lib/helium_proto/src/service/iot_config.proto" 12 | ]}, 13 | {out_dir, "src/grpc/autogen"}, 14 | {beam_out_dir, "src/grpc/autogen"}, 15 | {keep_beams, false}, 16 | {create_services, true}, 17 | {type, all}, 18 | {override_gpb_defaults, true}, 19 | {gpb_opts, [ 20 | use_packages, 21 | {defs_as_proplists, true}, 22 | {report, true}, 23 | {descriptor, false}, 24 | {recursive, false}, 25 | {i, "_build/default/lib/helium_proto/src"}, 26 | {o, "src/grpc/autogen"}, 27 | %% prevent name collisions with the server code 28 | {module_name_prefix, ""}, 29 | {module_name_suffix, "_pb"}, 30 | {rename, {msg_fqname, base_name}}, 31 | {rename, {msg_fqname, {prefix, {by_proto, [{iot_config, "iot_config_"}]}}}}, 32 | {rename, {msg_name, {suffix, "_pb"}}}, 33 | {strings_as_binaries, false}, 34 | type_specs 35 | ]} 36 | ]}. 37 | -------------------------------------------------------------------------------- /config/localhost: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/config/localhost -------------------------------------------------------------------------------- /config/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {libp2p, [ 3 | {random_peer_pred, fun router_utils:random_non_miner_predicate/1}, 4 | {use_dns_for_seeds, true}, 5 | {seed_dns_cname, "seed.helium.io"}, 6 | {seed_config_dns_name, "_seed_config.helium.io"}, 7 | {max_tcp_connections, 500000} 8 | ]}, 9 | {blockchain, [ 10 | {base_dir, "/var/data"}, 11 | {update_dir, "update"}, 12 | {port, 2154}, 13 | {seed_nodes, 14 | "/ip4/18.217.27.26/tcp/2154,/ip4/35.161.222.43/tcp/443,/ip4/99.80.158.114/tcp/2154,/ip4/3.66.43.167/tcp/443,/ip4/52.220.121.45/tcp/2154,/ip4/54.207.252.240/tcp/443,/ip4/3.34.10.207/tcp/2154,/ip4/13.238.174.45/tcp/443"}, 15 | {snap_source_base_url, "https://snapshots.helium.wtf/mainnet"}, 16 | {fetch_latest_from_snap_source, true}, 17 | {honor_quick_sync, true}, 18 | {quick_sync_mode, blessed_snapshot}, 19 | {blessed_snapshot_block_height, 1469991}, 20 | {blessed_snapshot_block_hash, 21 | <<228, 109, 63, 62, 128, 210, 153, 145, 214, 101, 72, 249, 132, 71, 169, 232, 182, 113, 22 | 179, 150, 254, 35, 44, 113, 116, 77, 197, 127, 48, 10, 6, 43>>}, 23 | {disable_gateway_cache, true}, 24 | {sync_timeout_mins, 1}, 25 | {max_inbound_connections, 12}, 26 | {outbound_gossip_connections, 4}, 27 | {sc_packet_handler, router_device_routing}, 28 | {sc_max_actors, 1100}, 29 | {sc_sup_type, server}, 30 | {sc_hook_close_submit, router_sc_worker}, 31 | {metadata_fun, fun router_utils:metadata_fun/0} 32 | ]}, 33 | {router, [ 34 | {max_v8_context, 1000}, 35 | {oui, 1}, 36 | {sc_open_dc_amount, 100000000}, 37 | {sc_expiration_interval, 25}, 38 | {sc_expiration_buffer, 15}, 39 | {router_console_api, [ 40 | {endpoint, <<"https://console.helium.com">>}, 41 | {ws_endpoint, <<"wss://console.helium.com/socket/router/websocket">>}, 42 | {downlink_endpoint, <<"https://console.helium.com">>}, 43 | {secret, <<>>} 44 | ]}, 45 | {device_queue_size_limit, 20}, 46 | {metrics_port, 3000}, 47 | {denylist_keys, ["1SbEYKju337P6aYsRd9DT2k4qgK5ZK62kXbSvnJgqeaxK3hqQrYURZjL"]}, 48 | {denylist_url, "https://api.github.com/repos/helium/denylist/releases/latest"} 49 | ]}, 50 | {grpcbox, [ 51 | {servers, [ 52 | #{ 53 | grpc_opts => #{ 54 | service_protos => [router_pb, state_channel_pb], 55 | services => #{ 56 | 'helium.packet_router.packet' => helium_packet_service 57 | } 58 | }, 59 | transport_opts => #{ssl => false}, 60 | listen_opts => #{ 61 | port => 8080, 62 | ip => {0, 0, 0, 0} 63 | }, 64 | pool_opts => #{size => 500}, 65 | server_opts => #{ 66 | header_table_size => 4096, 67 | enable_push => 1, 68 | max_concurrent_streams => unlimited, 69 | initial_window_size => 16777216, 70 | max_frame_size => 16384, 71 | max_header_list_size => unlimited 72 | } 73 | } 74 | ]} 75 | ]}, 76 | {chatterbox, [ 77 | {client_initial_window_size, 16777216}, 78 | {server_initial_window_size, 16777216} 79 | ]}, 80 | {prometheus, [ 81 | {collectors, [ 82 | prometheus_boolean, 83 | prometheus_counter, 84 | prometheus_gauge, 85 | prometheus_histogram, 86 | prometheus_vm_memory_collector, 87 | prometheus_vm_system_info_collector 88 | ]}, 89 | {vm_system_info_collector_metrics, [process_count]} 90 | ]}, 91 | {lager, [ 92 | {suppress_supervisor_start_stop, true}, 93 | {log_root, "/var/data/log"}, 94 | {crash_log, "crash.log"}, 95 | {handlers, [ 96 | {lager_file_backend, [ 97 | {file, "router.log"}, 98 | {level, warning}, 99 | {size, 0}, 100 | {date, "$D0"}, 101 | {count, 7}, 102 | {formatter_config, [ 103 | "[", 104 | date, 105 | " ", 106 | time, 107 | "] ", 108 | pid, 109 | " [", 110 | severity, 111 | "]", 112 | {device_id, [" [", device_id, "]"], ""}, 113 | " [", 114 | {module, ""}, 115 | {function, [":", function], ""}, 116 | {line, [":", line], ""}, 117 | "] ", 118 | message, 119 | "\n" 120 | ]} 121 | ]} 122 | ]}, 123 | {traces, [ 124 | {{lager_file_backend, "router.log"}, [{application, router}], info}, 125 | {{lager_file_backend, "router.log"}, [{module, router_console_api}], info}, 126 | {{lager_file_backend, "router.log"}, [{module, router_device_routing}], info}, 127 | 128 | {{lager_file_backend, "router.log"}, [{module, helium_packet_service}], info}, 129 | {{lager_file_backend, "router.log"}, [{module, router_ics_eui_worker}], info}, 130 | { 131 | {lager_file_backend, "router.log"}, 132 | [{module, router_ics_route_get_euis_handler}], 133 | info 134 | }, 135 | {{lager_file_backend, "router.log"}, [{module, router_ics_skf_list_handler}], info}, 136 | {{lager_file_backend, "router.log"}, [{module, router_ics_skf_worker}], info}, 137 | 138 | {{lager_file_backend, "state_channel.log"}, [{module, router_sc_worker}], info}, 139 | { 140 | {lager_file_backend, "state_channel.log"}, 141 | [{module, blockchain_state_channels_db_owner}], 142 | info 143 | }, 144 | { 145 | {lager_file_backend, "state_channel.log"}, 146 | [{module, blockchain_state_channels_cache}], 147 | info 148 | }, 149 | { 150 | {lager_file_backend, "state_channel.log"}, 151 | [{module, blockchain_state_channels_server}], 152 | info 153 | }, 154 | { 155 | {lager_file_backend, "state_channel.log"}, 156 | [{module, blockchain_state_channels_worker}], 157 | info 158 | }, 159 | {{lager_file_backend, "blockchain.log"}, [{module, blockchain_txn}], debug} 160 | ]} 161 | ]} 162 | ]. 163 | -------------------------------------------------------------------------------- /config/test.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | [ 3 | {router, [ 4 | {disable_preferred_hotspot, "false"}, 5 | {oui, 1}, 6 | {devaddr_prefix, "72"}, 7 | {metrics_port, 3001}, 8 | {max_v8_context, 10}, 9 | {router_http_channel_url_check, false}, 10 | {router_xor_filter_worker, false}, 11 | {charge_when_no_offer, true}, 12 | {charge_late_packets, true}, 13 | {ics, #{ 14 | transport => http, 15 | host => "localhost", 16 | port => 8085, 17 | devaddr_enabled => "true", 18 | eui_enabled => "true", 19 | skf_enabled => "true", 20 | route_id => "test-route-id" 21 | }}, 22 | {config_service_max_timeout_attempt, 5} 23 | ]}, 24 | {blockchain, [ 25 | {port, 3615}, 26 | {sc_sup_type, server} 27 | ]}, 28 | {grpcbox, [ 29 | {client, #{ 30 | channels => [ 31 | {ics_channel, [{http, "localhost", 8085, []}], #{}}, 32 | {default_channel, [{http, "localhost", 8080, []}], #{}}, 33 | {ics_location_channel, [{http, "localhost", 8085, []}], #{}} 34 | ] 35 | }}, 36 | {servers, [ 37 | #{ 38 | grpc_opts => #{ 39 | service_protos => [router_pb, state_channel_pb, packet_router_pb], 40 | services => #{ 41 | 'helium.router' => helium_router_service, 42 | 'helium.state_channel' => blockchain_grpc_sc_server_handler, 43 | 'helium.packet_router.packet' => helium_packet_service 44 | } 45 | }, 46 | transport_opts => #{ssl => false}, 47 | listen_opts => #{ 48 | port => 8080, 49 | ip => {0, 0, 0, 0} 50 | }, 51 | pool_opts => #{size => 2}, 52 | server_opts => #{ 53 | header_table_size => 4096, 54 | enable_push => 1, 55 | max_concurrent_streams => unlimited, 56 | initial_window_size => 65535, 57 | max_frame_size => 16384, 58 | max_header_list_size => unlimited 59 | } 60 | }, 61 | #{ 62 | grpc_opts => #{ 63 | service_protos => [iot_config_pb], 64 | services => #{ 65 | 'helium.iot_config.gateway' => router_test_ics_gateway_service, 66 | 'helium.iot_config.session_key_filter' => router_test_ics_skf_service, 67 | 'helium.iot_config.route' => router_test_ics_route_service 68 | } 69 | }, 70 | transport_opts => #{ssl => false}, 71 | listen_opts => #{ 72 | port => 8085, 73 | ip => {0, 0, 0, 0} 74 | }, 75 | pool_opts => #{size => 2}, 76 | server_opts => #{ 77 | header_table_size => 4096, 78 | enable_push => 1, 79 | max_concurrent_streams => unlimited, 80 | initial_window_size => 16777216, 81 | max_frame_size => 16384, 82 | max_header_list_size => unlimited 83 | } 84 | } 85 | ]} 86 | ]}, 87 | {chatterbox, [ 88 | {client_initial_window_size, 16777216}, 89 | {server_initial_window_size, 16777216} 90 | ]} 91 | ]. 92 | -------------------------------------------------------------------------------- /config/testnet.config.src: -------------------------------------------------------------------------------- 1 | [ 2 | {libp2p, [ 3 | {use_dns_for_seeds, true}, 4 | {seed_dns_cname, "seed.helium.io"}, 5 | {seed_config_dns_name, "_seed_config.helium.io"}, 6 | {nat_map, #{ 7 | {"${ROUTER_NAT_INTERNAL_IP}", "${ROUTER_NAT_INTERNAL_PORT}"} => 8 | {"${ROUTER_NAT_EXTERNAL_IP}", "${ROUTER_NAT_EXTERNAL_PORT}"} 9 | }}, 10 | {max_tcp_connections, 500000} 11 | ]}, 12 | {blockchain, [ 13 | {network, testnet}, 14 | {base_dir, "/var/data"}, 15 | {update_dir, "update"}, 16 | {port, 2154}, 17 | {seed_nodes, "/ip4/54.244.119.55/tcp/2154,/ip4/3.22.146.211/tcp/443"}, 18 | {snap_source_base_url, undefined}, 19 | {fetch_latest_from_snap_source, false}, 20 | {honor_quick_sync, false}, 21 | {quick_sync_mode, blessed_snapshot}, 22 | {blessed_snapshot_block_height, 249841}, 23 | {blessed_snapshot_block_hash, 24 | <<122, 187, 109, 20, 77, 150, 83, 249, 53, 117, 132, 251, 53, 237, 84, 91, 154, 229, 118, 129, 19, 70, 211, 25 | 209, 53, 222, 165, 20, 12, 244, 92, 201>>}, 26 | {disable_gateway_cache, true}, 27 | {sync_timeout_mins, 1}, 28 | {max_inbound_connections, 12}, 29 | {outbound_gossip_connections, 4}, 30 | {sc_packet_handler, router_device_routing}, 31 | {sc_max_actors, "${ROUTER_SC_MAX_ACTORS}"}, 32 | {sc_sup_type, server}, 33 | {sc_hook_close_submit, router_sc_worker} 34 | ]}, 35 | {router, [ 36 | {grpc_port, ${GRPC_PORT:-8080}}, 37 | {max_v8_context, 1000}, 38 | {oui, "${ROUTER_OUI}"}, 39 | {sc_open_dc_amount, "${ROUTER_SC_OPEN_DC_AMOUNT}"}, 40 | {sc_expiration_interval, "${ROUTER_SC_EXPIRATION_INTERVAL}"}, 41 | {sc_expiration_buffer, "${ROUTER_SC_EXPIRATION_BUFFER}"}, 42 | {max_sc_open, "${ROUTER_MAX_SC_OPEN}"}, 43 | {router_console_api, [ 44 | {endpoint, <<"${ROUTER_CONSOLE_ENDPOINT}">>}, 45 | {ws_endpoint, <<"${ROUTER_CONSOLE_WS_ENDPOINT}">>}, 46 | {downlink_endpoint, <<"${ROUTER_CONSOLE_DOWNLINK_ENDPOINT}">>}, 47 | {secret, <<"${ROUTER_CONSOLE_SECRET}">>} 48 | ]}, 49 | {router_xor_filter_worker, "${ROUTER_XOR_FILTER_WORKER}"}, 50 | {ics, #{ 51 | host => "${ROUTER_ICS_HOST}", 52 | port => "${ROUTER_ICS_PORT}" 53 | }}, 54 | {frame_timeout, "${ROUTER_FRAME_TIMEOUT}"}, 55 | {router_http_channel_url_check, "${ROUTER_HTTP_CHANNEL_URL_CHECK}"}, 56 | {disco_frame_timeout, "${ROUTER_DISCO_FRAME_TIMEOUT}"}, 57 | {device_queue_size_limit, "${ROUTER_DEVICE_QUEUE_SIZE_LIMIT}"} 58 | ]}, 59 | {grpcbox, [ 60 | {servers, [ 61 | #{ 62 | grpc_opts => #{ 63 | service_protos => [router_pb, state_channel_pb], 64 | services => #{ 65 | 'helium.state_channel' => blockchain_grpc_sc_server_handler 66 | } 67 | }, 68 | transport_opts => #{ssl => false}, 69 | listen_opts => #{ 70 | port => 8080, 71 | ip => {0, 0, 0, 0} 72 | }, 73 | pool_opts => #{size => 100}, 74 | server_opts => #{ 75 | header_table_size => 4096, 76 | enable_push => 1, 77 | max_concurrent_streams => unlimited, 78 | initial_window_size => 65535, 79 | max_frame_size => 16384, 80 | max_header_list_size => unlimited 81 | } 82 | } 83 | ]} 84 | ]}, 85 | {prometheus, [ 86 | {collectors, [ 87 | prometheus_boolean, 88 | prometheus_counter, 89 | prometheus_gauge, 90 | prometheus_histogram, 91 | prometheus_vm_memory_collector, 92 | prometheus_vm_system_info_collector 93 | ]}, 94 | {vm_system_info_collector_metrics, [process_count]} 95 | ]}, 96 | {lager, [ 97 | {suppress_supervisor_start_stop, true}, 98 | {log_root, "/var/data/log"}, 99 | {crash_log, "crash.log"}, 100 | {handlers, [ 101 | {lager_file_backend, [ 102 | {file, "router.log"}, 103 | {level, warning}, 104 | {size, 0}, 105 | {date, "$D0"}, 106 | {count, 7}, 107 | {formatter_config, [ 108 | "[", 109 | date, 110 | " ", 111 | time, 112 | "] ", 113 | pid, 114 | " [", 115 | severity, 116 | "]", 117 | {device_id, [" [", device_id, "]"], ""}, 118 | " [", 119 | {module, ""}, 120 | {function, [":", function], ""}, 121 | {line, [":", line], ""}, 122 | "] ", 123 | message, 124 | "\n" 125 | ]} 126 | ]} 127 | ]}, 128 | {traces, [ 129 | {{lager_file_backend, "router.log"}, [{application, router}], info}, 130 | {{lager_file_backend, "router.log"}, [{module, router_console_api}], info}, 131 | {{lager_file_backend, "router.log"}, [{module, router_device_routing}], info}, 132 | {{lager_file_backend, "state_channel.log"}, [{module, router_sc_worker}], info}, 133 | {{lager_file_backend, "state_channel.log"}, 134 | [{module, blockchain_state_channels_db_owner}], info}, 135 | {{lager_file_backend, "state_channel.log"}, [{module, blockchain_state_channels_cache}], 136 | info}, 137 | {{lager_file_backend, "state_channel.log"}, 138 | [{module, blockchain_state_channels_server}], info}, 139 | {{lager_file_backend, "state_channel.log"}, 140 | [{module, blockchain_state_channels_worker}], info}, 141 | {{lager_file_backend, "blockchain.log"}, [{module, blockchain_txn}], debug} 142 | ]} 143 | ]} 144 | ]. 145 | -------------------------------------------------------------------------------- /config/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name router@127.0.0.1 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie router 6 | 7 | ## Up the number of processes allowed 8 | +P 2097151 9 | 10 | # Tweak GC to run more often 11 | -env ERL_FULLSWEEP_AFTER 3 12 | -------------------------------------------------------------------------------- /docker-compose-local.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | router: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-CI 7 | image: quay.io/team-helium/router:local 8 | container_name: helium_router 9 | restart: always 10 | env_file: .env 11 | network_mode: host 12 | volumes: 13 | - "/var/data:/var/data" 14 | depends_on: 15 | - prometheus 16 | prometheus: 17 | image: prom/prometheus 18 | container_name: helium_prometheus 19 | network_mode: host 20 | volumes: 21 | - "./prometheus.yml:/etc/prometheus/prometheus.yml" 22 | node_exporter: 23 | image: quay.io/prometheus/node-exporter:latest 24 | container_name: helium_node_exporter 25 | command: 26 | - '--path.rootfs=/host' 27 | - "--collector.disable-defaults" 28 | - "--collector.filesystem" 29 | - "--collector.netstat" 30 | - "--collector.meminfo" 31 | - "--collector.cpu" 32 | - "--collector.loadavg" 33 | network_mode: host 34 | pid: host 35 | restart: unless-stopped 36 | volumes: 37 | - '/:/host:ro,rslave' 38 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | router: 4 | image: quay.io/team-helium/router:latest 5 | container_name: helium_router 6 | restart: always 7 | env_file: .env 8 | network_mode: host 9 | volumes: 10 | - "/var/data:/var/data" 11 | depends_on: 12 | - prometheus 13 | prometheus: 14 | image: prom/prometheus 15 | container_name: helium_prometheus 16 | network_mode: host 17 | volumes: 18 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 19 | node_exporter: 20 | image: quay.io/prometheus/node-exporter:latest 21 | container_name: helium_node_exporter 22 | command: 23 | - '--path.rootfs=/host' 24 | - "--collector.disable-defaults" 25 | - "--collector.filesystem" 26 | - "--collector.netstat" 27 | - "--collector.meminfo" 28 | - "--collector.cpu" 29 | - "--collector.loadavg" 30 | network_mode: host 31 | pid: host 32 | restart: unless-stopped 33 | volumes: 34 | - '/:/host:ro,rslave' 35 | -------------------------------------------------------------------------------- /docs/Device.md: -------------------------------------------------------------------------------- 1 | # Device CLI Commands 2 | 3 | Router maintains a database in RocksDB of Devices, but the paired Console instance should always be viewed as the source of truth for a Device. 4 | 5 | > XOR Filter commands have been relocated under `router filter`. 6 | 7 | > **NOTE: Commands in these docs assume an alias for `router`.** 8 | 9 | ```sh 10 | alias router="docker exec -it helium_router _build/default/rel/router/bin/router" 11 | ``` 12 | 13 | ## Commands 14 | 15 | ### List 16 | 17 | ```sh 18 | router device all 19 | ``` 20 | 21 | Queries Console to report all devices. 22 | 23 | ### Info 24 | 25 | ```sh 26 | router device --id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 27 | ``` 28 | 29 | Simple information for a device. `` is the device ID from Console, not the `app_eui` or `dev_eui`. 30 | 31 | ### Tracing 32 | 33 | ```sh 34 | router device trace --id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx [--stop] 35 | ``` 36 | 37 | Tracing a device will set all logs related to a device to debug level for `240 minutes`, writing to the file `{BASE_DIR}/traces/{first-5-chars-of-device-id}.log`. 38 | 39 | Information about the device considered for logging 40 | - `device_id` 41 | - `dev_eui` 42 | - `app_eui` 43 | - `devaddr` 44 | 45 | > **NOTE:** `devaddr` may result in some logs from other devices in places where the device cannot be accurately determined. 46 | 47 | Pass the `--stop` option to stop the trace before the `240 minutes` is over. 48 | 49 | Traces will not survive through restart. 50 | 51 | ### Device Queue 52 | 53 | ```sh 54 | router device queue --id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 55 | ``` 56 | 57 | Print out the downlink queue of a device. 58 | 59 | ```sh 60 | router device queue clear --id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 61 | ``` 62 | 63 | Empty the downlink queue of a device. 64 | 65 | ```sh 66 | router device add --id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --payload= [--channel-name= --ack= --port=] 67 | ``` 68 | 69 | | Option | Type | Default | Description | 70 | |------------------|---------|-------------------------------|--------------------------------------------| 71 | | `--payload` | string | None | Content to queue for the device | 72 | | `--channel-name` | string | `"Test cli downlink message"` | Name of integration to report in Console | 73 | | `--port` | number | `1` | Port to send downlink (`0` is not allowed) | 74 | | `--ack` | boolean | `false` | Confirmed or Unconfirmed downlink | 75 | 76 | ### Prune 77 | 78 | Router keeps a copy of devices in Rocksdb. If devices are removed from Console but not Router, this command will remove them from Router. 79 | 80 | > **NOTE:** This can take a while if you have a lot of devices in your Console. 81 | 82 | ```sh 83 | router device prune [--commit] 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/Organizations.md: -------------------------------------------------------------------------------- 1 | # Organizations CLI 2 | 3 | Router only cares about organizations from Console insofar as it can use DC for a device. 4 | 5 | Similar to Devices, Console is the source of truth for an organization's DC balance. 6 | 7 | > **NOTE: Commands in these docs assume an alias for `router`.** 8 | 9 | ```sh 10 | alias router="docker exec -it helium_router _build/default/rel/router/bin/router" 11 | ``` 12 | 13 | ## Commands 14 | 15 | ### List 16 | 17 | ```sh 18 | router organization info all [--less= --more=] 19 | ``` 20 | 21 | List all organizations, their `dc_balance` and `nonce`. 22 | 23 | ### Info 24 | 25 | ```sh 26 | router organization info 27 | ``` 28 | 29 | Single listing of `dc_balance` and `nonce` for ``. 30 | 31 | ### Refill 32 | 33 | ```sh 34 | router organization update -b [--commit] 35 | ``` 36 | 37 | A way to update an organization's DC balance in Console from Router. 38 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | Diagrams are generated using [PlantUML](https://plantuml.com/), which is 5 | open-source, free software available for Linux and other operating systems. 6 | 7 | Generate diagrams as Scalable Vector Graphics: 8 | 9 | plantuml -tsvg *.plantuml 10 | 11 | Without the `-tsvg` flag, it defaults to PNG images. 12 | 13 | On Debian-ish Linux such as Ubuntu, use latest .jar from plantuml.com 14 | to avoid minor rendering bugs. 15 | -------------------------------------------------------------------------------- /docs/XOR_Filter.md: -------------------------------------------------------------------------------- 1 | # XOR Filters CLI 2 | 3 | XOR Filters are part of the OUI purchased when running an instance of Router. 4 | The maximum allowed under an OUI is a [chain variable `max_xor_filter_num`](https://api.helium.io/v1/vars/max_xor_filter_num) (default: 5). 5 | 6 | Device credentials are hashed (DevEUI, AppEUI) into the filters to allow gateways to check where to send Join Requests. 7 | 8 | There is a fee to submit XOR txns with the chain based on the byte_size of the filter up to the [`max_xor_filter_size`](https://api.helium.io/v1/vars/max_xor_filter_size). 9 | 10 | > **NOTE: Commands in these docs assume an alias for `router`.** 11 | 12 | ```sh 13 | alias router="docker exec -it helium_router _build/default/rel/router/bin/router" 14 | ``` 15 | 16 | ## Commands 17 | 18 | ### When is the next update? 19 | 20 | ```sh 21 | router filter timer 22 | ``` 23 | 24 | Tells you how long until router checks for new devices with the paired Console. 25 | 26 | `router_xor_filter_worker` periodically checks for new devices. If there are new devices, it will pick the smallest filter in the OUI by size and add the new devices to it. 27 | 28 | By default, it will check every `10 minutes`. This can be adjusted with the application var `router_xor_filter_worker_timer` in ms. 29 | 30 | ### Force an update 31 | 32 | ```sh 33 | router filter update [--commit] 34 | ``` 35 | 36 | Checks with Console and reports any devices to be added to the XOR filters. 37 | 38 | If `--commit` is provided, it will add the devices to the XOR filters immediately, ignoring the timer. 39 | 40 | ### Report 41 | 42 | ```sh 43 | router filter report [--id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx] 44 | ``` 45 | 46 | Reports the binary size, and number of devices in each XOR filter. 47 | 48 | If `--id=` is passed, it will tell you which filter that device stored. 49 | 50 | ### RocksDB out of sync 51 | ```sh 52 | router filter reset_db [--commit] 53 | ``` 54 | 55 | Devices are stored in rocksdb to help determine existing residence of devices in the filters when router is started. If rocks becomes out of sync with the devices in Console, this command will re-insert only existing devices into the database. 56 | ### Moving devices around 57 | 58 | #### Migrate to another filter 59 | 60 | ```sh 61 | router filter migrate --from= --to= [--commit] 62 | ``` 63 | 64 | > **NOTE: XOR filters are 0-indexed.** 65 | 66 | Results in 2 txns. 67 | 68 | 1. Emptying the `` filter. 69 | 2. Combining devices in `` and `` into the `` filter. 70 | 71 | If you have 2 filters, you can use this command to move all devices to a single filter to reduce subsequent XOR txn fees. 72 | 73 | ``` sh 74 | router filter migrate --from=4 --to=0 --commit 75 | ``` 76 | 77 | This will remove all devices from filter 4 and merge them into filter 0. 78 | Causing 2 txns to be submitted. 79 | - New larger filter 0 80 | - Empty filter 4 81 | 82 | #### Move to Front 83 | 84 | ```sh 85 | filter move_to_front [--commit] 86 | ``` 87 | 88 | `router_xor_filter_worker` tries to put new devices into the smallest available filter, eventually this will result in equally distributed filters. 89 | 90 | This command will take all devices from Console and split them amongst `` filters. 91 | 92 | ``` sh 93 | router filter move_to_front 2 --commit 94 | ``` 95 | 96 | If you ran this command with the following filters... 97 | 98 | | Filter | Num Devices | 99 | |--------|-------------| 100 | | 0 | 876 | 101 | | 1 | 368 | 102 | | 2 | 360 | 103 | | 3 | 378 | 104 | | 4 | 366 | 105 | 106 | You would end up with... 107 | 108 | | Filter | Num Devices | 109 | |--------|-------------| 110 | | 0 | 1174 | 111 | | 1 | 1174 | 112 | | 2 | 0 | 113 | | 3 | 0 | 114 | | 4 | 0 | 115 | 116 | This would allow the XOR Filters to be updated with new devices without incuring large txn fees. 117 | 118 | #### Rebalancing 119 | 120 | > **NOTE: Not recommended, this will result in every filter having the same txn fee.** 121 | 122 | ```sh 123 | router filter rebalance [--commit] 124 | ``` 125 | 126 | Evenly distribute devices amongst existing filters. 127 | 128 | -------------------------------------------------------------------------------- /docs/decoder.plantuml: -------------------------------------------------------------------------------- 1 | ' To generate diagrams, run: plantuml -tsvg *.plantuml 2 | ' On Debian-ish Linux, use latest .jar from plantuml.com 3 | 4 | @startuml 5 | 6 | header Router dispatching Decoder via V8 JavaScript engine 7 | footer Page %page% of %lastpage% 8 | 9 | ' Preserve same sequence across all diagrams: 10 | ' (intentionally omitted: Lager) 11 | actor APIs as api order 1 12 | collections "Blockchain\nCore" as core order 10 13 | entity "State\nChannel" as sc order 20 14 | box "Exists entirely within router's BEAM image" 15 | participant "Listener\nfor gRPC\n(ranch)" as listener order 30 16 | 'control "Router\ninit/1" as init order 40 17 | participant "Channels\n(Integrations)" as channels order 50 18 | participant Device as device order 60 19 | database ETS as ets order 61 20 | participant Metrics as metrics order 70 21 | participant DevAddr as devaddr order 80 22 | participant Decoder as decoder order 90 23 | participant "XOR\nFilter" as xor order 99 24 | end box 25 | database "Router\nDB" as db order 100 26 | participant "V8\n(JS)" as v8 order 200 27 | 28 | title 29 | Decode LoRaWAN frame payload via JavaScript in V8 30 | Evaluating a custom decoder function 31 | end title 32 | 33 | skinparam sequence { 34 | LifeLineBorderColor #LightSteelBlue 35 | LifeLineBackgroundColor #Salmon 36 | MessageAlign direction 37 | } 38 | 39 | ... 40 | 41 | [-> listener ++: Encoded payload 42 | note right: gRPC service via grpcbox 43 | listener -> channels --++ : gen_event 44 | channels -> channels : add_sup_handler 45 | note left: ./src/channels/router_channel.erl 46 | 47 | channels -> device ++ : gen_server 48 | note left: ./src/device/router_device_channels_worker.erl 49 | 50 | device -> device : handle_cast:\nhandle_join or\nhandle_frame 51 | note right: Cache by UUID 52 | 53 | device -> device : handle_cast:\nframe_timeout 54 | device -> device : send_join_to_channel()\n or\nsend_data_to_channel() 55 | channels <- device -- : encode_payload() 56 | note right: Yes, it is called "encode_payload" despite initiating "decode" 57 | 58 | channels -> decoder ++ : decode() 59 | note left 60 | ./src/decoders/router_decoder.erl 61 | ./src/decoders/router_decoder_custom_sup.erl 62 | end note 63 | decoder -> ets ++ : lookup() 64 | decoder <- ets -- : custom_decoder\n JavaScript function definition 65 | decoder -> decoder : gen_server\nhandle_call:\ndecode 66 | note left: ./src/decoders/router_decoder_custom_worker.erl 67 | decoder -> v8 ++ : erlang_v8:call() 68 | decoder <- v8 -- : Decoded 69 | channels <- decoder -- : Decoded 70 | 71 | [<- channels : gen_event notify 72 | note right 73 | ./src/channels/router_channel.erl 74 | Deliver decoded payload to channel subscribers ("integrations") 75 | e.g., ./src/channels/router_http_channel.erl 76 | end note 77 | 78 | channels ->o api -- : Decoded 79 | note right: ./src/channels/router_console_channel.erl 80 | 81 | ... 82 | 83 | @enduml 84 | -------------------------------------------------------------------------------- /docs/decoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/docs/decoder.png -------------------------------------------------------------------------------- /docs/erlang-processes.plantuml: -------------------------------------------------------------------------------- 1 | ' To generate diagrams, run: plantuml -tsvg *.plantuml 2 | ' On Debian-ish Linux, use latest .jar from plantuml.com 3 | 4 | @startuml 5 | 6 | header Erlang processes within Router 7 | footer Page %page% of %lastpage% 8 | 9 | ' Preserve same sequence across all diagrams: 10 | ' (intentionally omitted: Lager) 11 | actor APIs as api order 1 12 | collections "Blockchain\nCore" as core order 10 13 | entity "State\nChannel" as sc order 20 14 | box "Exists entirely within router's BEAM image" 15 | participant "Listener\nfor gRPC\n(ranch)" as listener order 30 16 | control "Router\ninit/1" as init order 40 17 | participant "Channels\n(Integrations)" as channels order 50 18 | participant Device as device order 60 19 | database ETS as ets order 61 20 | participant Metrics as metrics order 70 21 | participant DevAddr as devaddr order 80 22 | participant Decoder as decoder order 90 23 | participant "XOR\nFilter" as xor order 99 24 | end box 25 | database "Router\nDB" as db order 100 26 | participant "V8\n(JS)" as v8 order 200 27 | 28 | title Router Start of Persistent Erlang Processes\n(some have remote components) 29 | 30 | skinparam sequence { 31 | LifeLineBorderColor #LightSteelBlue 32 | LifeLineBackgroundColor #Salmon 33 | MessageAlign direction 34 | } 35 | 36 | activate init #Salmon 37 | 38 | init ->o ets : init_ets/0\nets:new/2 39 | note right 40 | ETS: in-memory data store 41 | ./src/decoders/router_decoder.erl 42 | end note 43 | 44 | init ->o listener : application:\n ensure_all_started/1 45 | note right 46 | RPC using Google ProtoBufs listens via ranch socket server 47 | ./src/grpc/helium_router_service.erl, grpc_lib, grpc_client 48 | Template rendered via ./_build/default/lib/grpc_*/erlang.mk 49 | end note 50 | 51 | init ->o core : init supervisor 52 | note right 53 | See GitHub: helium/blockchain-core/ 54 | Nested init: lock, swarm, event, score_cache, 55 | worker, txn_mgr, state_channel, ... 56 | end note 57 | 58 | init ->o metrics : init worker 59 | note left: ./src/metrics/router_metrics.erl 60 | 61 | init ->o db : init worker 62 | note left: ./src/router_db.erl 63 | 64 | init -> device ++ : init supervisor 65 | note right: ./src/device/router_devices_sup.erl 66 | device -> device : init worker 67 | note right: router_device_worker 68 | device ->o ets : ets:new/2 69 | note right 70 | ./src/device/router_device_routing.erl 71 | ./src/device/router_device_cache.erl 72 | end note 73 | device -> channels --++ : init supervisor 74 | note right: ./src/device/router_device_worker.erl 75 | channels ->o channels -- : init worker 76 | note right: ./src/device/router_device_channels_worker.erl 77 | 78 | init ->o sc : init worker 79 | note right: ./src/router_sc_worker.erl State Channels 80 | 81 | init -> api ++ : init supervisor 82 | note right: ./src/apis/router_console_sup.erl 83 | api -> api : init worker 84 | note right 85 | router_console_api 86 | router_console_ws_worker 87 | router_console_dc_tracker 88 | end note 89 | api ->o ets -- : ets:new/2 90 | note right 91 | ./src/apis/router_console_api.erl 92 | ./src/apis/router_console_dc_tracker.erl 93 | end note 94 | 95 | init -> decoder ++ : init supervisor 96 | note left 97 | Payload extraction 98 | ./src/decoders/router_decoder_sup.erl 99 | end note 100 | decoder -> decoder : init worker 101 | note left 102 | For eval of user-defined JavaScript functions 103 | ./src/decoders/router_decoder_custom_sup.erl 104 | end note 105 | decoder ->o v8 : init worker 106 | note left: ./src/decoders/router_v8.erl JavaScript for Integrations 107 | decoder ->o ets : ets:new/2 108 | decoder ->o decoder -- : init worker 109 | note left: ./src/decoders/router_decoder_custom_worker.erl 110 | 111 | init ->o devaddr : init worker 112 | note left: ./src/device/ LoRaWAN device addresses 113 | init ->o xor : init worker 114 | note left: ./src/router_xor_filter_worker.erl Filter devices 115 | 116 | ... 117 | 118 | @enduml 119 | -------------------------------------------------------------------------------- /docs/erlang-processes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/docs/erlang-processes.png -------------------------------------------------------------------------------- /include/lorawan.hrl: -------------------------------------------------------------------------------- 1 | % 2 | % Copyright (c) 2016-2019 Petr Gotthard 3 | % All rights reserved. 4 | % Distributed under the terms of the MIT License. See the LICENSE file. 5 | % 6 | 7 | -record(stat, { 8 | time, 9 | lati, 10 | long, 11 | alti, 12 | rxnb, 13 | rxok, 14 | rxfw, 15 | ackr, 16 | dwnb, 17 | txnb, 18 | % TTN extensions 19 | mail, 20 | desc 21 | }). 22 | 23 | -record(frame, {conf, devaddr, adr, adr_ack_req, ack, fcnt, fopts, port, data}). 24 | 25 | -define(to_record(Record, Object, Default), 26 | list_to_tuple([Record | [maps:get(X, Object, Default) || X <- record_info(fields, Record)]]) 27 | ). 28 | 29 | -define(to_record(Record, Object), ?to_record(Record, Object, undefined)). 30 | 31 | -define(to_map(Record, RecData), 32 | maps:from_list( 33 | lists:filtermap( 34 | fun 35 | ({_K, D, D}) -> false; 36 | ({K, V, _D}) -> {true, {K, V}} 37 | end, 38 | lists:zip3( 39 | record_info(fields, Record), 40 | lorawan_db:record_fields(RecData), 41 | tl(tuple_to_list(#Record{})) 42 | ) 43 | ) 44 | ) 45 | ). 46 | 47 | -define(REALM, <<"lorawan-server">>). 48 | 49 | -record(config, { 50 | name :: nonempty_string(), 51 | admin_url :: string(), 52 | items_per_page :: integer(), 53 | slack_token :: 'undefined' | string(), 54 | email_from :: 'undefined' | string(), 55 | email_server :: 'undefined' | string(), 56 | email_user :: 'undefined' | string(), 57 | email_password :: 'undefined' | string() 58 | }). 59 | 60 | -record(user, { 61 | name :: nonempty_string(), 62 | pass_ha1 :: string(), 63 | scopes :: [string()], 64 | email :: string(), 65 | send_alerts :: boolean() 66 | }). 67 | 68 | -record(server, { 69 | sname :: atom(), 70 | router_perf :: [{calendar:datetime(), {integer(), integer()}}] 71 | }). 72 | 73 | -record(event, { 74 | evid :: binary(), 75 | severity :: atom(), 76 | first_rx :: calendar:datetime(), 77 | last_rx :: calendar:datetime(), 78 | count :: integer(), 79 | entity :: atom(), 80 | eid :: binary(), 81 | text :: binary(), 82 | args :: 'undefined' | binary() 83 | }). 84 | 85 | % end of file 86 | -------------------------------------------------------------------------------- /include/lorawan_adr.hrl: -------------------------------------------------------------------------------- 1 | %% For every packet offer, call `lorawan_adr:track_offer/2` with this 2 | %% record. 3 | %% 4 | %% Packet offers contain limited information, but we will attempt to 5 | %% glean what we can from them (which is gateway diversity). 6 | -record(adr_offer, { 7 | hotspot :: libp2p_crypto:pubkey_bin(), 8 | packet_hash :: binary() 9 | }). 10 | 11 | %% For every uplink, call `lorawan_adr:track_packet/2` with this 12 | %% record. 13 | %% 14 | %% ## From the LoRaWAN Specification section 4.3.1.1: 15 | %% 16 | %% ------------------------------------------------------------------- 17 | %% ### wants_adr 18 | %% ------------------------------------------------------------------- 19 | %% 20 | %% If the uplink ADR bit is set, the network will control the data 21 | %% rate and Tx power of the end-device through the appropriate MAC 22 | %% commands. If the ADR bit is not set, the network will not attempt 23 | %% to control the data rate nor the transmit power of the end-device 24 | %% regardless of the received signal quality. The network MAY still 25 | %% send commands to change the Channel mask or the frame repetition 26 | %% parameters. 27 | %% 28 | %% 29 | %% ------------------------------------------------------------------- 30 | %% ### wants_adr_ack 31 | %% ------------------------------------------------------------------- 32 | %% 33 | %% If an end-device’s data rate is optimized by the network to use a 34 | %% data rate higher than its default data rate, or a TXPower lower 35 | %% than its default TXPower, it periodically needs to validate that 36 | %% the network still receives the uplink frames. Each time the uplink 37 | %% frame counter is incremented (for each new uplink, repeated 38 | %% transmissions do not increase the counter), the device increments 39 | %% an ADR_ACK_CNT counter. After ADR_ACK_LIMIT uplinks (ADR_ACK_CNT >= 40 | %% ADR_ACK_LIMIT) without any downlink response, it sets the ADR 41 | %% acknowledgment request bit (ADRACKReq). The network is required to 42 | %% respond with a downlink frame within the next ADR_ACK_DELAY frames, 43 | %% any received downlink frame following an uplink frame resets the 44 | %% ADR_ACK_CNT counter. The downlink ACK bit does not need to be set 45 | %% as any response during the receive slot of the end-device indicates 46 | %% that the gateway has still received the uplinks from this 47 | %% device. If no reply is received within the next ADR_ACK_DELAY 48 | %% uplinks (i.e., after a total of ADR_ACK_LIMIT + ADR_ACK_DELAY), the 49 | %% end-device MUST try to regain connectivity by first stepping up the 50 | %% transmit power to default power if possible then switching to the 51 | %% next lower data rate that provides a longer radio range. The 52 | %% end-device MUST further lower its data rate step by step every time 53 | %% ADR_ACK_DELAY is reached. Once the device has reached the lowest 54 | %% data rate, it MUST re-enable all default uplink frequency channels. 55 | %% 56 | %% 57 | %% ------------------------------------------------------------------- 58 | %% ### TX Power (derived) 59 | %% ------------------------------------------------------------------- 60 | %% 61 | %% Default Tx Power is the maximum transmission power allowed for the 62 | %% device considering device capabilities and regional regulatory 63 | %% constraints. Device shall use this power level, until the network 64 | %% asks for less, through the LinkADRReq MAC command. 65 | -record(adr_packet, { 66 | packet_hash :: binary(), 67 | wants_adr :: boolean(), 68 | wants_adr_ack :: boolean(), 69 | datarate_config :: {lorawan_utils:spreading(), lorawan_utils:bandwidth()}, 70 | snr :: float(), 71 | rssi :: float(), 72 | hotspot :: libp2p_crypto:pubkey_bin() 73 | }). 74 | 75 | %% All data here is derived from an uplinked `LinkADRAns' MAC command. 76 | -record(adr_answer, { 77 | %% Not used by ADR. However, devices complying the LoRaWan spec 78 | %% _should not_ make any adjustments if it rejects any of these 79 | %% settings. 80 | channel_mask_ack :: boolean(), 81 | datarate_ack :: boolean(), 82 | power_ack :: boolean() 83 | }). 84 | -------------------------------------------------------------------------------- /include/lorawan_vars.hrl: -------------------------------------------------------------------------------- 1 | -define(JOIN_REQ, 2#000). 2 | -define(JOIN_ACCEPT, 2#001). 3 | -define(CONFIRMED_UP, 2#100). 4 | -define(UNCONFIRMED_UP, 2#010). 5 | -define(CONFIRMED_DOWN, 2#101). 6 | -define(UNCONFIRMED_DOWN, 2#011). 7 | -define(RFU, 2#110). 8 | -define(PRIORITY, 2#111). 9 | 10 | -define(RX_DELAY, 0). 11 | -define(FRAME_TIMEOUT, 200). 12 | -define(JOIN_TIMEOUT, 2000). 13 | -------------------------------------------------------------------------------- /include/metrics.hrl: -------------------------------------------------------------------------------- 1 | -define(METRICS_TICK_INTERVAL, timer:seconds(10)). 2 | -define(METRICS_TICK, '__router_metrics_tick'). 3 | 4 | -define(METRICS_ROUTING_PACKET, "router_device_routing_packet_duration"). 5 | -define(METRICS_CONSOLE_API, "router_console_api_duration"). 6 | -define(METRICS_WS, "router_ws_state"). 7 | -define(METRICS_VM_CPU, "router_vm_cpu"). 8 | -define(METRICS_VM_PROC_Q, "router_vm_process_queue"). 9 | -define(METRICS_VM_ETS_MEMORY, "router_vm_ets_memory"). 10 | -define(METRICS_DEVICE_TOTAL, "router_device_total_gauge"). 11 | -define(METRICS_DEVICE_RUNNING, "router_device_running_gauge"). 12 | -define(METRICS_CONSOLE_POOL, "router_console_pool_gauge"). 13 | 14 | -define(METRICS, [ 15 | {?METRICS_ROUTING_PACKET, prometheus_histogram, [type, status, reason, downlink], 16 | "Routing Packet duration"}, 17 | {?METRICS_CONSOLE_API, prometheus_histogram, [type, status], "Console API duration"}, 18 | {?METRICS_WS, prometheus_boolean, [], "Websocket State"}, 19 | {?METRICS_VM_CPU, prometheus_gauge, [cpu], "Router CPU usage"}, 20 | {?METRICS_VM_PROC_Q, prometheus_gauge, [name], "Router process queue"}, 21 | {?METRICS_VM_ETS_MEMORY, prometheus_gauge, [name], "Router ets memory"}, 22 | {?METRICS_DEVICE_TOTAL, prometheus_gauge, [], "Device total gauge"}, 23 | {?METRICS_DEVICE_RUNNING, prometheus_gauge, [], "Device running gauge"}, 24 | {?METRICS_CONSOLE_POOL, prometheus_gauge, [name, type], "Console pool gauge"} 25 | ]). 26 | -------------------------------------------------------------------------------- /include/router_device.hrl: -------------------------------------------------------------------------------- 1 | -record(device_v7, { 2 | id :: binary() | undefined, 3 | name :: binary() | undefined, 4 | dev_eui :: binary() | undefined, 5 | app_eui :: binary() | undefined, 6 | %% {nwk_s_key, app_s_key} 7 | keys = [] :: list({binary() | undefined, binary() | undefined}), 8 | devaddrs = [] :: [binary()], 9 | dev_nonces = [] :: [binary()], 10 | fcnt = undefined :: non_neg_integer() | undefined, 11 | fcntdown = 0 :: non_neg_integer(), 12 | offset = 0 :: non_neg_integer(), 13 | channel_correction = false :: boolean(), 14 | queue = [] :: [any()], 15 | region :: atom() | undefined, 16 | last_known_datarate :: integer() | undefined, 17 | ecc_compact :: map(), 18 | location :: libp2p_crypto:pubkey_bin() | undefined, 19 | metadata = #{} :: map(), 20 | is_active = true :: boolean() 21 | }). 22 | 23 | -record(device_v6, { 24 | id :: binary() | undefined, 25 | name :: binary() | undefined, 26 | dev_eui :: binary() | undefined, 27 | app_eui :: binary() | undefined, 28 | %% {nwk_s_key, app_s_key} 29 | keys = [] :: list({binary() | undefined, binary() | undefined}), 30 | devaddr :: binary() | undefined, 31 | dev_nonces = [] :: [binary()], 32 | fcnt = undefined :: undefined | non_neg_integer(), 33 | fcntdown = 0 :: non_neg_integer(), 34 | offset = 0 :: non_neg_integer(), 35 | channel_correction = false :: boolean(), 36 | queue = [] :: [any()], 37 | region :: atom() | undefined, 38 | last_known_datarate :: integer() | undefined, 39 | ecc_compact :: map(), 40 | location :: libp2p_crypto:pubkey_bin() | undefined, 41 | metadata = #{} :: map(), 42 | is_active = true :: boolean() 43 | }). 44 | 45 | -record(device_v5, { 46 | id :: binary() | undefined, 47 | name :: binary() | undefined, 48 | dev_eui :: binary() | undefined, 49 | app_eui :: binary() | undefined, 50 | %% {nwk_s_key, app_s_key} 51 | keys = [] :: list({binary() | undefined, binary() | undefined}), 52 | devaddr :: binary() | undefined, 53 | dev_nonces = [] :: [binary()], 54 | fcnt = undefined :: undefined | non_neg_integer(), 55 | fcntdown = 0 :: non_neg_integer(), 56 | offset = 0 :: non_neg_integer(), 57 | channel_correction = false :: boolean(), 58 | queue = [] :: [any()], 59 | ecc_compact :: map(), 60 | location :: libp2p_crypto:pubkey_bin() | undefined, 61 | metadata = #{} :: map(), 62 | is_active = true :: boolean() 63 | }). 64 | 65 | -record(device_v4, { 66 | id :: binary() | undefined, 67 | name :: binary() | undefined, 68 | dev_eui :: binary() | undefined, 69 | app_eui :: binary() | undefined, 70 | nwk_s_key :: binary() | undefined, 71 | app_s_key :: binary() | undefined, 72 | devaddr :: binary() | undefined, 73 | join_nonce = <<>> :: binary(), 74 | fcnt = undefined :: undefined | non_neg_integer(), 75 | fcntdown = 0 :: non_neg_integer(), 76 | offset = 0 :: non_neg_integer(), 77 | channel_correction = false :: boolean(), 78 | queue = [] :: [any()], 79 | keys :: map(), 80 | location :: libp2p_crypto:pubkey_bin() | undefined, 81 | metadata = #{} :: map(), 82 | is_active = true :: boolean() 83 | }). 84 | 85 | -record(device_v3, { 86 | id :: binary() | undefined, 87 | name :: binary() | undefined, 88 | dev_eui :: binary() | undefined, 89 | app_eui :: binary() | undefined, 90 | nwk_s_key :: binary() | undefined, 91 | app_s_key :: binary() | undefined, 92 | devaddr :: binary() | undefined, 93 | join_nonce = <<>> :: binary(), 94 | fcnt = undefined :: undefined | non_neg_integer(), 95 | fcntdown = 0 :: non_neg_integer(), 96 | offset = 0 :: non_neg_integer(), 97 | channel_correction = false :: boolean(), 98 | queue = [] :: [any()], 99 | keys :: map(), 100 | location :: libp2p_crypto:pubkey_bin() | undefined, 101 | metadata = #{} :: map() 102 | }). 103 | 104 | -record(device_v2, { 105 | id :: binary() | undefined, 106 | name :: binary() | undefined, 107 | dev_eui :: binary() | undefined, 108 | app_eui :: binary() | undefined, 109 | nwk_s_key :: binary() | undefined, 110 | app_s_key :: binary() | undefined, 111 | join_nonce = <<>> :: binary(), 112 | fcnt = undefined :: undefined | non_neg_integer(), 113 | fcntdown = 0 :: non_neg_integer(), 114 | offset = 0 :: non_neg_integer(), 115 | channel_correction = false :: boolean(), 116 | queue = [] :: [any()], 117 | keys :: map(), 118 | metadata = #{} :: map() 119 | }). 120 | 121 | -record(device_v1, { 122 | id :: binary() | undefined, 123 | name :: binary() | undefined, 124 | dev_eui :: binary() | undefined, 125 | app_eui :: binary() | undefined, 126 | nwk_s_key :: binary() | undefined, 127 | app_s_key :: binary() | undefined, 128 | join_nonce = <<>> :: binary(), 129 | fcnt = undefined :: undefined | non_neg_integer(), 130 | fcntdown = 0 :: non_neg_integer(), 131 | offset = 0 :: non_neg_integer(), 132 | channel_correction = false :: boolean(), 133 | queue = [] :: [any()], 134 | key :: map() | undefined 135 | }). 136 | 137 | -record(device, { 138 | id :: binary() | undefined, 139 | name :: binary() | undefined, 140 | dev_eui :: binary() | undefined, 141 | app_eui :: binary() | undefined, 142 | nwk_s_key :: binary() | undefined, 143 | app_s_key :: binary() | undefined, 144 | join_nonce = <<>> :: binary(), 145 | fcnt = undefined :: undefined | non_neg_integer(), 146 | fcntdown = 0 :: non_neg_integer(), 147 | offset = 0 :: non_neg_integer(), 148 | channel_correction = false :: boolean(), 149 | queue = [] :: [any()] 150 | }). 151 | -------------------------------------------------------------------------------- /include/router_device_worker.hrl: -------------------------------------------------------------------------------- 1 | -define(DOWNLINK_REGIONS, [ 2 | 'US915', 3 | 'AU915', 4 | 'EU868', 5 | 'CN470', 6 | 'AS923_1', 7 | 'AS923_2', 8 | 'AS923_3', 9 | 'AS923_4' 10 | ]). 11 | 12 | -type downlink_region() :: 13 | 'US915' | 'AU915' | 'EU868' | 'CN470' | 'AS923_1' | 'AS923_2' | 'AS923_3' | 'AS923_4'. 14 | 15 | -record(frame, { 16 | mtype, 17 | devaddr, 18 | ack = 0, 19 | adr = 0, 20 | adrackreq = 0, 21 | rfu = 0, 22 | fpending = 0, 23 | fcnt, 24 | fopts = [], 25 | fport, 26 | data 27 | }). 28 | 29 | -record(join_accept_args, { 30 | region :: atom(), 31 | app_nonce :: binary(), 32 | dev_addr :: binary(), 33 | app_key :: binary() 34 | }). 35 | 36 | -record(join_cache, { 37 | uuid :: router_utils:uuid_v4(), 38 | rssi :: float(), 39 | app_key :: binary(), 40 | packet_selected :: 41 | { 42 | Packet :: blockchain_helium_packet_v1:packet(), 43 | PubKeyBin :: libp2p_crypto:pubkey_bin(), 44 | Region :: atom(), 45 | PacketTime :: non_neg_integer(), 46 | HoldTime :: non_neg_integer() 47 | }, 48 | packets = [] :: [ 49 | { 50 | Packet :: blockchain_helium_packet_v1:packet(), 51 | PubKeyBin :: libp2p_crypto:pubkey_bin(), 52 | Region :: atom(), 53 | PacketTime :: non_neg_integer(), 54 | HoldTime :: non_neg_integer() 55 | } 56 | ], 57 | pid :: pid() 58 | }). 59 | 60 | -record(frame_cache, { 61 | uuid :: router_utils:uuid_v4(), 62 | rssi :: float(), 63 | count = 1 :: pos_integer(), 64 | packet :: blockchain_helium_packet_v1:packet(), 65 | pubkey_bin :: libp2p_crypto:pubkey_bin(), 66 | frame :: #frame{}, 67 | pid :: pid(), 68 | region :: atom(), 69 | pubkey_bins = [] :: [libp2p_crypto:pubkey_bin()] 70 | }). 71 | 72 | -record(downlink, { 73 | confirmed :: boolean(), 74 | port :: non_neg_integer(), 75 | payload :: binary(), 76 | channel :: router_channel:channel(), 77 | region :: downlink_region() 78 | }). 79 | -------------------------------------------------------------------------------- /priv/genesis.mainnet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/priv/genesis.mainnet -------------------------------------------------------------------------------- /priv/genesis.testnet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/priv/genesis.testnet -------------------------------------------------------------------------------- /prometheus-template.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 10s 4 | external_labels: 5 | server: helium-router 6 | 7 | scrape_configs: 8 | - job_name: router 9 | static_configs: 10 | - targets: ['127.0.0.1:3000'] 11 | 12 | - job_name: node 13 | static_configs: 14 | - targets: ["localhost:9100"] 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | {parse_transform, lager_transform}, 4 | warnings_as_errors 5 | ]}. 6 | 7 | {cover_export_enabled, true}. 8 | 9 | {cover_enabled, true}. 10 | 11 | {deps, [ 12 | {clique, ".*", {git, "https://github.com/helium/clique.git", {branch, "develop"}}}, 13 | {erlang_lorawan, ".*", 14 | {git, "https://github.com/helium/erlang-lorawan.git", {branch, "master"}}}, 15 | {router_utils, ".*", {git, "https://github.com/helium/router-utils.git", {branch, "main"}}}, 16 | {lager, "3.9.2"}, 17 | {jsx, "3.1.0"}, 18 | {kvc, {git, "https://github.com/etrepum/kvc", {tag, "v1.7.0"}}}, 19 | {observer_cli, "1.7.1"}, 20 | {hackney, "1.18.1"}, 21 | {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.7"}}}, 22 | {httpc_aws, {git, "https://github.com/helium/httpc-aws.git", {branch, "master"}}}, 23 | {websocket_client, "1.4.2"}, 24 | {erlang_v8, {git, "https://github.com/Vagabond/erlang_v8.git", {branch, "master"}}}, 25 | {blockchain, {git, "https://github.com/helium/blockchain-core.git", {tag, "validator1.17.2"}}}, 26 | {prometheus, "4.8.1"}, 27 | {elli, "3.3.0"}, 28 | {bbmustache, {git, "https://github.com/helium/bbmustache", {branch, "data-fun"}}}, 29 | {iso8601, {git, "https://github.com/erlsci/iso8601", {branch, "master"}}}, 30 | {grpcbox, {git, "https://github.com/novalabsxyz/grpcbox.git", {branch, "adt/immediate-sends"}}}, 31 | {inet_cidr, "1.1.0", {pkg, erl_cidr}}, 32 | {throttle, "0.3.0", {pkg, lambda_throttle}}, 33 | {e2qc, {git, "https://github.com/helium/e2qc.git", {branch, "master"}}}, 34 | {helium_proto, {git, "https://github.com/helium/proto.git", {branch, "master"}}} 35 | ]}. 36 | 37 | {plugins, [ 38 | {grpcbox_plugin, 39 | {git, "https://github.com/novalabsxyz/grpcbox_plugin.git", 40 | {branch, "andymck/ts-master/combined-opts-and-template-changes"}}}, 41 | {rebar3_format, "1.0.1"}, 42 | {erlfmt, "1.0.0"} 43 | ]}. 44 | 45 | {format, [ 46 | {ignore, ["src/grpc/autogen/**/*"]}, 47 | {files, [ 48 | "rebar.config", 49 | "rebar-deps.config", 50 | "{src,include,test}/**/*.{hrl,erl,app.src}", 51 | "config/{sys,test}.{config,config.src}" 52 | ]}, 53 | {formatter, erlfmt_formatter}, 54 | {options, #{print_width => 100, ignore_pragma => true}} 55 | ]}. 56 | 57 | {xref_checks, [ 58 | undefined_function_calls, 59 | undefined_functions, 60 | locals_not_used 61 | ]}. 62 | 63 | {xref_ignores, [ 64 | validator_pb, 65 | validator_client_pb, 66 | router_pb, 67 | router_client_pb, 68 | packet_router_pb, 69 | packet_router_client_pb, 70 | state_channel_pb, 71 | state_channel_client_pb, 72 | gateway_client_pb, 73 | iot_config_client_pb, 74 | iot_config_pb 75 | ]}. 76 | 77 | {profiles, [ 78 | {test, [ 79 | {overrides, [ 80 | {add, blockchain, [{erl_opts, [{d, 'TEST'}]}]} 81 | ]}, 82 | {deps, [ 83 | {meck, "0.9.2"}, 84 | {elli_websocket, "0.1.1"} 85 | ]} 86 | ]}, 87 | {mainnet, []}, 88 | {testnet, [ 89 | {relx, [ 90 | {sys_config_src, "config/testnet.config.src"} 91 | ]} 92 | ]} 93 | ]}. 94 | 95 | {relx, [ 96 | {release, {router, "1.0.0"}, [router]}, 97 | {vm_args, "config/vm.args"}, 98 | {sys_config, "config/sys.config"}, 99 | {sys_config_src, "config/sys.config.src"}, 100 | {include_src, true}, 101 | {overlay, [ 102 | {copy, "priv/genesis", "update/genesis"}, 103 | {copy, "./_build/default/lib/blockchain/scripts/extensions/peer", "bin/extensions/peer"}, 104 | {copy, "./_build/default/lib/blockchain/scripts/extensions/ledger", 105 | "bin/extensions/ledger"}, 106 | {copy, "./_build/default/lib/blockchain/scripts/extensions/trace", "bin/extensions/trace"}, 107 | {copy, "./_build/default/lib/blockchain/scripts/extensions/txn", "bin/extensions/txn"}, 108 | {copy, "./_build/default/lib/blockchain/scripts/extensions/repair", 109 | "bin/extensions/repair"}, 110 | {copy, "./_build/default/lib/blockchain/scripts/extensions/sc", "bin/extensions/sc"}, 111 | {copy, "./_build/default/lib/blockchain/scripts/extensions/snapshot", 112 | "bin/extensions/snapshot"}, 113 | {copy, "./scripts/extensions/organization", "bin/extensions/organization"}, 114 | {copy, "./scripts/extensions/device", "bin/extensions/device"}, 115 | {copy, "./scripts/extensions/info", "bin/extensions/info"}, 116 | {copy, "./scripts/extensions/filter", "bin/extensions/filter"}, 117 | {copy, "./scripts/extensions/migration", "bin/extensions/migration"} 118 | ]}, 119 | {extended_start_script_hooks, [ 120 | {post_start, [ 121 | {wait_for_process, blockchain_worker} 122 | ]} 123 | ]}, 124 | {generate_start_script, true}, 125 | {extended_start_script, true}, 126 | {extended_start_script_extensions, [ 127 | {peer, "extensions/peer"}, 128 | {ledger, "extensions/ledger"}, 129 | {trace, "extensions/trace"}, 130 | {txn, "extensions/txn"}, 131 | {repair, "extensions/repair"}, 132 | {sc, "extensions/sc"}, 133 | {snapshot, "extensions/snapshot"}, 134 | {organization, "extensions/organization"}, 135 | {device, "extensions/device"}, 136 | {info, "extensions/info"}, 137 | {filter, "extensions/filter"}, 138 | {migration, "extensions/migration"} 139 | ]} 140 | ]}. 141 | 142 | {pre_hooks, [{"(linux)", ct, "./c_src/compile.sh"}]}. 143 | 144 | {ct_opts, [{sys_config, "config/test.config"}]}. 145 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helium/router/240544820ce410af453ba132b69c7a68c799bcd9/rebar3 -------------------------------------------------------------------------------- /scripts/extensions/device: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -t 0 ] ; then 3 | CLIQUE_COLUMNS=$(stty size 2>/dev/null | cut -d ' ' -f 2) 4 | export CLIQUE_COLUMNS 5 | fi 6 | 7 | j=1 8 | l=$# 9 | buf="[[\"device\"," 10 | while [ $j -le $l ]; do 11 | buf="$buf\"$1\"," 12 | j=$(( j + 1 )) 13 | shift 14 | done 15 | 16 | buf="${buf%?}]]" 17 | 18 | relx_nodetool rpc router_console command "$buf" 19 | exit $? 20 | -------------------------------------------------------------------------------- /scripts/extensions/filter: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -t 0 ] ; then 3 | CLIQUE_COLUMNS=$(stty size 2>/dev/null | cut -d ' ' -f 2) 4 | export CLIQUE_COLUMNS 5 | fi 6 | 7 | j=1 8 | l=$# 9 | buf="[[\"filter\"," 10 | while [ $j -le $l ]; do 11 | buf="$buf\"$1\"," 12 | j=$(( j + 1 )) 13 | shift 14 | done 15 | 16 | buf="${buf%?}]]" 17 | 18 | relx_nodetool rpc router_console command "$buf" 19 | exit $? 20 | -------------------------------------------------------------------------------- /scripts/extensions/info: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -t 0 ] ; then 3 | CLIQUE_COLUMNS=$(stty size 2>/dev/null | cut -d ' ' -f 2) 4 | export CLIQUE_COLUMNS 5 | fi 6 | 7 | j=1 8 | l=$# 9 | buf="[[\"info\"," 10 | while [ $j -le $l ]; do 11 | buf="$buf\"$1\"," 12 | j=$(( j + 1 )) 13 | shift 14 | done 15 | 16 | buf="${buf%?}]]" 17 | 18 | relx_nodetool rpc router_console command "$buf" 19 | exit $? 20 | -------------------------------------------------------------------------------- /scripts/extensions/migration: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -t 0 ] ; then 3 | CLIQUE_COLUMNS=$(stty size 2>/dev/null | cut -d ' ' -f 2) 4 | export CLIQUE_COLUMNS 5 | fi 6 | 7 | # extend relx default timeout (60s) if it has not already been overwritten 8 | if [ $RELX_RPC_TIMEOUT -eq 60 ] ; then 9 | export RELX_RPC_TIMEOUT=900 10 | fi 11 | 12 | j=1 13 | l=$# 14 | buf="[[\"migration\"," 15 | while [ $j -le $l ]; do 16 | buf="$buf\"$1\"," 17 | j=$(( j + 1 )) 18 | shift 19 | done 20 | 21 | buf="${buf%?}]]" 22 | 23 | relx_nodetool rpc router_console command "$buf" 24 | exit $? 25 | -------------------------------------------------------------------------------- /scripts/extensions/organization: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -t 0 ] ; then 3 | CLIQUE_COLUMNS=$(stty size 2>/dev/null | cut -d ' ' -f 2) 4 | export CLIQUE_COLUMNS 5 | fi 6 | 7 | j=1 8 | l=$# 9 | buf="[[\"organization\"," 10 | while [ $j -le $l ]; do 11 | buf="$buf\"$1\"," 12 | j=$(( j + 1 )) 13 | shift 14 | done 15 | 16 | buf="${buf%?}]]" 17 | 18 | relx_nodetool rpc router_console command "$buf" 19 | exit $? 20 | -------------------------------------------------------------------------------- /scripts/monitor_blocks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | alias router='sudo docker exec -it helium_router _build/default/rel/router/bin/router' 5 | 6 | INTERVAL=30 7 | 8 | for arg in "$@" 9 | do 10 | case $arg in 11 | -i|--interval) 12 | INTERVAL="$2" 13 | shift # remove argument name from processing 14 | shift # remove argument value from processing 15 | esac 16 | done 17 | 18 | while true; 19 | do 20 | chain_head=$(curl --silent https://api.helium.io/v1/blocks/height | cut -c"19-24"); 21 | router_head=$(router info height | cut -c"8-13"); 22 | echo "$(date) -- $router_head / $chain_head == $(($chain_head - $router_head)) behind"; 23 | sleep $INTERVAL; 24 | done 25 | -------------------------------------------------------------------------------- /scripts/save_logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | slugify () { 5 | echo "$@" | iconv -c -t ascii//TRANSLIT | sed -E 's/[~^]+//g' | sed -E 's/[^a-zA-Z0-9]+/-/g' | sed -E 's/^-+|-+$//g' | tr A-Z a-z 6 | } 7 | 8 | save_logs () { 9 | Slug=$(slugify $@); 10 | CurrDate=$(date +"%Y-%m-%d"); 11 | DirName="${CurrDate}_${Slug}" 12 | mkdir $DirName 13 | echo "created $DirName copying logs" 14 | cp -a /var/data/log/. $DirName 15 | echo "Logs copied to $DirName" 16 | } 17 | 18 | save_logs $@; 19 | -------------------------------------------------------------------------------- /src/apis/router_console_sup.erl: -------------------------------------------------------------------------------- 1 | -module(router_console_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | -define(WORKER(I, Args), #{ 12 | id => I, 13 | start => {I, start_link, Args}, 14 | restart => permanent, 15 | shutdown => 5000, 16 | type => worker, 17 | modules => [I] 18 | }). 19 | 20 | -define(FLAGS, #{ 21 | strategy => rest_for_one, 22 | intensity => 1, 23 | period => 5 24 | }). 25 | 26 | -define(SERVER, ?MODULE). 27 | 28 | %%==================================================================== 29 | %% API functions 30 | %%==================================================================== 31 | start_link() -> 32 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 33 | 34 | %%==================================================================== 35 | %% Supervisor callbacks 36 | %%==================================================================== 37 | init([]) -> 38 | DeviceAPIData = maps:from_list(application:get_env(router, router_console_api, [])), 39 | {ok, 40 | {?FLAGS, [ 41 | ?WORKER(router_console_api, [DeviceAPIData]), 42 | ?WORKER(router_console_ws_worker, [DeviceAPIData]), 43 | ?WORKER(router_console_dc_tracker, [#{}]) 44 | ]}}. 45 | 46 | %%==================================================================== 47 | %% Internal functions 48 | %%==================================================================== 49 | -------------------------------------------------------------------------------- /src/channels/router_console_channel.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router Console Channel == 4 | %% 5 | %% @end 6 | %%%------------------------------------------------------------------- 7 | -module(router_console_channel). 8 | 9 | -behaviour(gen_event). 10 | 11 | %% ------------------------------------------------------------------ 12 | %% gen_event Function Exports 13 | %% ------------------------------------------------------------------ 14 | -export([ 15 | init/1, 16 | handle_event/2, 17 | handle_call/2, 18 | handle_info/2, 19 | terminate/2, 20 | code_change/3 21 | ]). 22 | 23 | -record(state, { 24 | channel :: router_channel:channel(), 25 | device :: router:device() 26 | }). 27 | 28 | %% ------------------------------------------------------------------ 29 | %% gen_server Function Definitions 30 | %% ------------------------------------------------------------------ 31 | init({[Channel, Device], _}) -> 32 | ok = router_utils:lager_md(Device), 33 | lager:info("init with ~p", [Channel]), 34 | {ok, #state{channel = Channel, device = Device}}. 35 | 36 | handle_event({join, UUIDRef, Data}, #state{channel = Channel} = State0) -> 37 | State1 = 38 | case router_channel:receive_joins(Channel) of 39 | true -> do_handle_event(UUIDRef, Data, State0); 40 | false -> State0 41 | end, 42 | {ok, State1}; 43 | handle_event({data, UUIDRef, Data}, #state{} = State0) -> 44 | State1 = do_handle_event(UUIDRef, Data, State0), 45 | {ok, State1}; 46 | handle_event(_Msg, State) -> 47 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 48 | {ok, State}. 49 | 50 | handle_call(_Msg, State) -> 51 | lager:warning("rcvd unknown call msg: ~p", [_Msg]), 52 | {ok, ok, State}. 53 | 54 | handle_info(_Msg, State) -> 55 | lager:debug("rcvd unknown info msg: ~p", [_Msg]), 56 | {ok, State}. 57 | 58 | code_change(_OldVsn, State, _Extra) -> 59 | {ok, State}. 60 | 61 | terminate(_Reason, _State) -> 62 | ok. 63 | 64 | %% ------------------------------------------------------------------ 65 | %% Internal Function Definitions 66 | %% ------------------------------------------------------------------ 67 | 68 | -spec do_handle_event( 69 | UUIDRef :: router_utils:uuid_v4(), 70 | Data :: map(), 71 | #state{} 72 | ) -> #state{}. 73 | do_handle_event(UUIDRef, Data, #state{channel = Channel} = State) -> 74 | Pid = router_channel:controller(Channel), 75 | Report = #{ 76 | status => success, 77 | description => <<"console debug">>, 78 | request => #{body => router_channel:encode_data(Channel, Data)} 79 | }, 80 | ok = router_device_channels_worker:report_request(Pid, UUIDRef, Channel, Report), 81 | State. 82 | -------------------------------------------------------------------------------- /src/channels/router_no_channel.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router no Channel == 4 | %% 5 | %% Reports packets to Console when no integrations 6 | %% are configured for a device. 7 | %% 8 | %% @end 9 | %%%------------------------------------------------------------------- 10 | -module(router_no_channel). 11 | 12 | -behaviour(gen_event). 13 | 14 | %% ------------------------------------------------------------------ 15 | %% gen_event Function Exports 16 | %% ------------------------------------------------------------------ 17 | -export([ 18 | init/1, 19 | handle_event/2, 20 | handle_call/2, 21 | handle_info/2, 22 | terminate/2, 23 | code_change/3 24 | ]). 25 | 26 | -record(state, {channel :: router_channel:channel()}). 27 | 28 | %% ------------------------------------------------------------------ 29 | %% gen_server Function Definitions 30 | %% ------------------------------------------------------------------ 31 | init({[Channel, Device], _}) -> 32 | ok = router_utils:lager_md(Device), 33 | lager:info("init with ~p", [Channel]), 34 | {ok, #state{channel = Channel}}. 35 | 36 | handle_event({join, UUIDRef, Data}, #state{channel = Channel} = State0) -> 37 | State1 = 38 | case router_channel:receive_joins(Channel) of 39 | true -> do_handle_event(UUIDRef, Data, State0); 40 | false -> State0 41 | end, 42 | {ok, State1}; 43 | handle_event({data, UUIDRef, Data}, #state{} = State0) -> 44 | State1 = do_handle_event(UUIDRef, Data, State0), 45 | {ok, State1}; 46 | handle_event(_Msg, State) -> 47 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 48 | {ok, State}. 49 | 50 | handle_call(_Msg, State) -> 51 | lager:warning("rcvd unknown call msg: ~p", [_Msg]), 52 | {ok, ok, State}. 53 | 54 | handle_info({ping, _}, State) -> 55 | {ok, State}; 56 | handle_info(_Msg, State) -> 57 | lager:warning("rcvd unknown info msg: ~p", [_Msg]), 58 | {ok, State}. 59 | 60 | code_change(_OldVsn, State, _Extra) -> 61 | {ok, State}. 62 | 63 | terminate(_Reason, _State) -> 64 | ok. 65 | 66 | %% ------------------------------------------------------------------ 67 | %% Internal Function Definitions 68 | %% ------------------------------------------------------------------ 69 | -spec do_handle_event( 70 | UUIDRef :: router_utils:uuid_v4(), 71 | Data :: map(), 72 | #state{} 73 | ) -> #state{}. 74 | do_handle_event(UUIDRef, _Data, #state{channel = Channel} = State) -> 75 | Pid = router_channel:controller(Channel), 76 | Report = #{ 77 | status => no_channel, 78 | description => <<"no channels configured">>, 79 | request => #{} 80 | }, 81 | router_device_channels_worker:report_request(Pid, UUIDRef, Channel, Report), 82 | State. 83 | -------------------------------------------------------------------------------- /src/cli/router_cli_info.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc router_cli_info 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | -module(router_cli_info). 6 | 7 | -behavior(clique_handler). 8 | 9 | -export([register_cli/0]). 10 | 11 | -define(USAGE, fun(_, _, _) -> usage end). 12 | 13 | -include("router_device_worker.hrl"). 14 | 15 | register_cli() -> 16 | register_all_usage(), 17 | register_all_cmds(). 18 | 19 | register_all_usage() -> 20 | lists:foreach( 21 | fun(Args) -> apply(clique, register_usage, Args) end, 22 | [info_usage()] 23 | ). 24 | 25 | register_all_cmds() -> 26 | lists:foreach( 27 | fun(Cmds) -> [apply(clique, register_command, Cmd) || Cmd <- Cmds] end, 28 | [info_cmd()] 29 | ). 30 | 31 | %%-------------------------------------------------------------------- 32 | %% info 33 | %%-------------------------------------------------------------------- 34 | 35 | info_usage() -> 36 | [ 37 | ["info"], 38 | [ 39 | "\n\n", 40 | "info height - Get height of the blockchain for this router\n", 41 | " The first number is the current election epoch, and the second is\n" 42 | " the block height. If the second number is displayed with an asterisk (*)\n" 43 | " this node has yet to sync past the assumed valid hash in the node config.\n\n" 44 | "info name - Get name for this router\n" 45 | "info block_age - Get age of the latest block in the chain, in seconds.\n" 46 | "info device - Device's stats\n" 47 | "info hotspot - Device's stats\n" 48 | ] 49 | ]. 50 | 51 | info_cmd() -> 52 | [ 53 | [["info", "height"], [], [], fun info_height/3], 54 | [["info", "name"], [], [], fun info_name/3], 55 | [["info", "block_age"], [], [], fun info_block_age/3], 56 | [["info", "device", '*'], [], [], fun info_device/3], 57 | [["info", "hotspot", '*'], [], [], fun info_hotspot/3] 58 | ]. 59 | 60 | info_height(["info", "height"], [], []) -> 61 | {ok, Height} = router_blockchain:height(), 62 | {ok, SyncHeight} = router_blockchain:sync_height(), 63 | {ok, HeadBlock} = router_blockchain:head_block(), 64 | {Epoch0, _} = blockchain_block_v1:election_info(HeadBlock), 65 | Epoch = integer_to_list(Epoch0), 66 | case SyncHeight == Height of 67 | true -> 68 | [clique_status:text(Epoch ++ "\t\t" ++ integer_to_list(Height))]; 69 | false -> 70 | [ 71 | clique_status:text([ 72 | Epoch, 73 | "\t\t", 74 | integer_to_list(Height), 75 | "\t\t", 76 | integer_to_list(SyncHeight), 77 | "*" 78 | ]) 79 | ] 80 | end; 81 | info_height([_, _, _], [], []) -> 82 | usage. 83 | 84 | info_name(["info", "name"], [], []) -> 85 | {ok, Name} = erl_angry_purple_tiger:animal_name( 86 | libp2p_crypto:bin_to_b58(router_blockchain:pubkey_bin()) 87 | ), 88 | [clique_status:text(Name)]; 89 | info_name([_, _, _], [], []) -> 90 | usage. 91 | 92 | info_block_age(["info", "block_age"], [], []) -> 93 | Age = erlang:system_time(seconds) - router_blockchain:head_block_time(), 94 | [clique_status:text(integer_to_list(Age))]; 95 | info_block_age([_, _, _], [], []) -> 96 | usage. 97 | 98 | info_device(["info", "device", DeviceID0], [], []) -> 99 | DeviceID = erlang:list_to_binary(DeviceID0), 100 | Formatted = format(router_device_stats:lookup_device(DeviceID)), 101 | c_table(Formatted). 102 | 103 | info_hotspot(["info", "hotspot", HotspotB580], [], []) -> 104 | Hotspot = libp2p_crypto:b58_to_bin(HotspotB580), 105 | Formatted = format(router_device_stats:lookup_hotspot(Hotspot)), 106 | c_table(Formatted). 107 | 108 | -spec format(list()) -> list(). 109 | format(List) -> 110 | lists:map( 111 | fun({DeviceID, HotspotName, HotspotB58, Counter}) -> 112 | [ 113 | {device, DeviceID}, 114 | {hotspot_name, HotspotName}, 115 | {hotspot_b58, HotspotB58}, 116 | {conflicts, Counter} 117 | ] 118 | end, 119 | List 120 | ). 121 | 122 | -spec c_table(list(proplists:proplist()) | proplists:proplist()) -> clique_status:status(). 123 | c_table(PropLists) -> [clique_status:table(PropLists)]. 124 | -------------------------------------------------------------------------------- /src/cli/router_cli_registry.erl: -------------------------------------------------------------------------------- 1 | -module(router_cli_registry). 2 | 3 | -define(CLI_MODULES, [ 4 | router_cli_device_worker, 5 | router_cli_info, 6 | router_cli_xor_filter, 7 | router_cli_organization, 8 | router_cli_migration 9 | ]). 10 | 11 | -export([register_cli/0]). 12 | 13 | register_cli() -> 14 | Modules = 15 | case router_xor_filter_worker:enabled() of 16 | true -> ?CLI_MODULES; 17 | false -> lists:delete(router_cli_xor_filter, ?CLI_MODULES) 18 | end, 19 | clique:register(Modules). 20 | -------------------------------------------------------------------------------- /src/cli/router_console.erl: -------------------------------------------------------------------------------- 1 | -module(router_console). 2 | 3 | -export([command/1]). 4 | 5 | -spec command([string()]) -> ok. 6 | command(Cmd) -> 7 | clique:run(Cmd). 8 | -------------------------------------------------------------------------------- /src/decoders/router_decoder.erl: -------------------------------------------------------------------------------- 1 | -module(router_decoder). 2 | 3 | %% ------------------------------------------------------------------ 4 | %% Decoder API Exports 5 | %% ------------------------------------------------------------------ 6 | -export([ 7 | new/3, 8 | id/1, 9 | type/1, 10 | args/1 11 | ]). 12 | 13 | %% ------------------------------------------------------------------ 14 | %% API Exports 15 | %% ------------------------------------------------------------------ 16 | -export([ 17 | init_ets/0, 18 | add/1, 19 | delete/1, 20 | decode/4 21 | ]). 22 | 23 | -ifdef(TEST). 24 | -include_lib("eunit/include/eunit.hrl"). 25 | -endif. 26 | 27 | -define(ETS, router_decoder_ets). 28 | 29 | -record(decoder, { 30 | id :: binary(), 31 | type :: atom(), 32 | args :: map() 33 | }). 34 | 35 | -type decoder() :: #decoder{}. 36 | 37 | -export_type([decoder/0]). 38 | 39 | %% ------------------------------------------------------------------ 40 | %% Decoder Type Functions 41 | %% ------------------------------------------------------------------ 42 | 43 | -spec new(binary(), atom(), map()) -> decoder(). 44 | new(ID, Type, Args) -> 45 | #decoder{id = ID, type = Type, args = Args}. 46 | 47 | -spec id(decoder()) -> binary(). 48 | id(#decoder{id = ID}) -> 49 | ID. 50 | 51 | -spec type(decoder()) -> atom(). 52 | type(Decoder) -> 53 | Decoder#decoder.type. 54 | 55 | -spec args(decoder()) -> map(). 56 | args(Decoder) -> 57 | Decoder#decoder.args. 58 | 59 | %% ------------------------------------------------------------------ 60 | %% API functions 61 | %% ------------------------------------------------------------------ 62 | 63 | -spec init_ets() -> ok. 64 | init_ets() -> 65 | ?ETS = ets:new(?ETS, [public, named_table, set]), 66 | ok. 67 | 68 | -spec add(decoder()) -> ok | {error, any()}. 69 | add(Decoder) -> 70 | add(?MODULE:type(Decoder), Decoder). 71 | 72 | -spec delete(binary()) -> ok. 73 | delete(ID) -> 74 | true = ets:delete(?ETS, ID), 75 | ok. 76 | 77 | -spec decode( 78 | DecoderID :: binary(), 79 | Payload :: binary(), 80 | Port :: integer(), 81 | UplinkDetails :: map() 82 | ) -> {ok, any()} | {error, any()}. 83 | decode(DecoderID, Payload, Port, UplinkDetails) -> 84 | try decode_(DecoderID, Payload, Port, UplinkDetails) of 85 | {_Type, {ok, _} = OK} -> 86 | OK; 87 | {_Type, {error, _} = Err} -> 88 | Err 89 | catch 90 | _Class:_Reason:_Stacktrace -> 91 | lager:error("decoder ~p crashed: ~p (~p) stacktrace ~p", [ 92 | DecoderID, 93 | _Reason, 94 | Payload, 95 | _Stacktrace 96 | ]), 97 | {error, decoder_crashed} 98 | end. 99 | 100 | %% ------------------------------------------------------------------ 101 | %% Internal Function Definitions 102 | %% ------------------------------------------------------------------ 103 | 104 | -spec decode_( 105 | DecoderID :: binary(), 106 | Payload :: binary(), 107 | Port :: integer(), 108 | UplinkDetails :: map() 109 | ) -> {DecoderType :: atom(), {ok, DecodedPayload :: any()}} | {atom(), {error, any()}}. 110 | decode_(DecoderID, Payload, Port, UplinkDetails) -> 111 | case lookup(DecoderID) of 112 | {error, not_found} -> 113 | {unknown_decoder, {error, unknown_decoder}}; 114 | {ok, #decoder{type = custom} = Decoder} -> 115 | {custom, 116 | router_decoder_custom_sup:decode( 117 | Decoder, 118 | erlang:binary_to_list(Payload), 119 | Port, 120 | UplinkDetails 121 | )}; 122 | {ok, #decoder{type = cayenne} = Decoder} -> 123 | {cayenne, router_decoder_cayenne:decode(Decoder, Payload, Port)}; 124 | {ok, #decoder{type = browan_object_locator} = Decoder} -> 125 | {browan_object_locator, 126 | router_decoder_browan_object_locator:decode(Decoder, Payload, Port)}; 127 | {ok, _Decoder} -> 128 | {unhandled_decoder, {error, unhandled_decoder}} 129 | end. 130 | 131 | -spec add(atom(), decoder()) -> ok | {error, any()}. 132 | add(custom, Decoder) -> 133 | case router_decoder_custom_sup:add(Decoder) of 134 | {error, _Reason} = Error -> Error; 135 | {ok, _Pid} -> insert(Decoder) 136 | end; 137 | add(cayenne, Decoder) -> 138 | insert(Decoder); 139 | add(browan_object_locator, Decoder) -> 140 | insert(Decoder); 141 | add(_Type, _Decoder) -> 142 | {error, unhandled_decoder}. 143 | 144 | -spec lookup(DecoderID :: binary()) -> {ok, decoder()} | {error, not_found}. 145 | lookup(DecoderID) -> 146 | case ets:lookup(?ETS, DecoderID) of 147 | [] -> {error, not_found}; 148 | [{DecoderID, Decoder}] -> {ok, Decoder} 149 | end. 150 | 151 | -spec insert(decoder()) -> ok. 152 | insert(Decoder) -> 153 | ID = ?MODULE:id(Decoder), 154 | true = ets:insert(?ETS, {ID, Decoder}), 155 | ok. 156 | 157 | %% ------------------------------------------------------------------ 158 | %% EUNIT Tests 159 | %% ------------------------------------------------------------------ 160 | -ifdef(TEST). 161 | 162 | new_test() -> 163 | Decoder = #decoder{id = <<"id">>, type = custom, args = #{}}, 164 | ?assertEqual(Decoder, new(<<"id">>, custom, #{})). 165 | 166 | id_test() -> 167 | Decoder = new(<<"id">>, custom, #{}), 168 | ?assertEqual(<<"id">>, id(Decoder)). 169 | 170 | type_test() -> 171 | Decoder = new(<<"id">>, custom, #{}), 172 | ?assertEqual(custom, type(Decoder)). 173 | 174 | args_test() -> 175 | Decoder = new(<<"id">>, custom, #{}), 176 | ?assertEqual(#{}, args(Decoder)). 177 | 178 | add_test() -> 179 | Decoder = new(<<"id">>, unkown, #{}), 180 | ?assertEqual({error, unhandled_decoder}, add(Decoder)). 181 | 182 | insert_lookup_delete_test() -> 183 | _ = init_ets(), 184 | ID = <<"id">>, 185 | Decoder = new(ID, custom, #{}), 186 | ok = insert(Decoder), 187 | ok = delete(ID), 188 | ?assertEqual({error, not_found}, lookup(ID)), 189 | true = ets:delete(?ETS). 190 | 191 | -endif. 192 | -------------------------------------------------------------------------------- /src/decoders/router_decoder_browan_object_locator.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @doc 3 | %%% == Browan Object Locator Decoder == 4 | %%% 5 | %%% Browan Docs PDF [https://www.browan.com/download/DBk/stream] 6 | %%% 7 | %%% @end 8 | %%%------------------------------------------------------------------- 9 | -module(router_decoder_browan_object_locator). 10 | 11 | -export([decode/3]). 12 | 13 | -spec decode(router_decoder:decoder(), binary(), integer()) -> {ok, binary()} | {error, any()}. 14 | decode( 15 | _Decoder, 16 | <<0:1/integer, 0:1/integer, 0:1/integer, GNSError:1/integer, GNSFix:1/integer, _:1/integer, 17 | Moving:1/integer, Button:1/integer, BatteryPercent:4/unsigned-integer, 18 | Battery:4/unsigned-integer, _:1/integer, Temp:7/unsigned-integer, 19 | Lat:32/integer-signed-little, TempLon:24/integer-signed-little, Accuracy:3/integer, 20 | I:5/integer-unsigned>>, 21 | 136 22 | ) -> 23 | <> = <>, 24 | {ok, #{ 25 | gns_error => GNSError == 1, 26 | gns_fix => GNSFix == 1, 27 | moving => Moving == 1, 28 | button => Button == 1, 29 | battery => (25 + Battery) / 10, 30 | battery_percent => 100 * (BatteryPercent / 15), 31 | temperature => Temp - 32, 32 | latitude => Lat / 1000000, 33 | longitude => Lon / 1000000, 34 | accuracy => trunc(math:pow(2, Accuracy + 2)) 35 | }}; 36 | decode( 37 | _Decoder, 38 | <<0:8/integer, UpdateIntervalWhileMoving:16/integer-unsigned-little, 1:8/integer, 39 | KeepAliveIntervalWhileStationary:16/integer-unsigned-little, 2:8/integer, 40 | GSensorTimeoutWhenMoving:16/integer-unsigned-little, _/binary>>, 41 | 204 42 | ) -> 43 | {ok, #{ 44 | update_interval_moving => UpdateIntervalWhileMoving, 45 | keepalive_interval_stationary => KeepAliveIntervalWhileStationary, 46 | gsensor_timeout_moving => GSensorTimeoutWhenMoving 47 | }}; 48 | decode(_Decoder, _, _) -> 49 | {error, browan_decoder_failure}. 50 | -------------------------------------------------------------------------------- /src/decoders/router_decoder_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc router decoder supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(router_decoder_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SUP(I, Args), #{ 17 | id => I, 18 | start => {I, start_link, Args}, 19 | restart => permanent, 20 | shutdown => 5000, 21 | type => supervisor, 22 | modules => [I] 23 | }). 24 | 25 | -define(WORKER(I, Args), #{ 26 | id => I, 27 | start => {I, start_link, Args}, 28 | restart => permanent, 29 | shutdown => 5000, 30 | type => worker, 31 | modules => [I] 32 | }). 33 | 34 | -define(FLAGS, #{ 35 | strategy => rest_for_one, 36 | intensity => 1, 37 | period => 5 38 | }). 39 | 40 | -define(SERVER, ?MODULE). 41 | 42 | %%==================================================================== 43 | %% API functions 44 | %%==================================================================== 45 | 46 | start_link() -> 47 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 48 | 49 | %%==================================================================== 50 | %% Supervisor callbacks 51 | %%==================================================================== 52 | 53 | %% Child :: #{id => Id, start => {M, F, A}} 54 | %% Optional keys are restart, shutdown, type, modules. 55 | %% Before OTP 18 tuples must be used to specify a child. e.g. 56 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 57 | init([]) -> 58 | {ok, 59 | {?FLAGS, [ 60 | ?WORKER(router_v8, [#{}]), 61 | ?SUP(router_decoder_custom_sup, []) 62 | ]}}. 63 | 64 | %%==================================================================== 65 | %% Internal functions 66 | %%==================================================================== 67 | -------------------------------------------------------------------------------- /src/decoders/router_v8.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router v8 == 4 | %% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_v8). 7 | 8 | -behavior(gen_server). 9 | 10 | -ifdef(TEST). 11 | -include_lib("eunit/include/eunit.hrl"). 12 | -endif. 13 | 14 | %% ------------------------------------------------------------------ 15 | %% API Function Exports 16 | %% ------------------------------------------------------------------ 17 | -export([ 18 | start_link/1, 19 | get/0 20 | ]). 21 | 22 | %% ------------------------------------------------------------------ 23 | %% gen_server Function Exports 24 | %% ------------------------------------------------------------------ 25 | -export([ 26 | init/1, 27 | handle_call/3, 28 | handle_cast/2, 29 | handle_info/2, 30 | terminate/2, 31 | code_change/3 32 | ]). 33 | 34 | -define(SERVER, ?MODULE). 35 | -define(REG_NAME, router_v8_vm). 36 | -define(VM_CHECK_TICK, pull_data_timeout_tick). 37 | -define(VM_CHECK_TIMER, timer:minutes(2)). 38 | -define(VM_CHECK_FUN_NAME, <<"vm_check">>). 39 | -define(VM_CHECK_FUN, <<"function ", ?VM_CHECK_FUN_NAME/binary, "() {return 0;}">>). 40 | 41 | -record(state, {vm :: pid()}). 42 | 43 | %% ------------------------------------------------------------------ 44 | %% API Function Definitions 45 | %% ------------------------------------------------------------------ 46 | start_link(Args) -> 47 | gen_server:start_link({local, ?SERVER}, ?SERVER, Args, []). 48 | 49 | -spec get() -> {ok, pid()}. 50 | get() -> 51 | {ok, erlang:whereis(?REG_NAME)}. 52 | 53 | %% ------------------------------------------------------------------ 54 | %% gen_server Function Definitions 55 | %% ------------------------------------------------------------------ 56 | init(_Args) -> 57 | lager:info("~p init with ~p", [?SERVER, _Args]), 58 | {ok, VM} = erlang_v8:start_vm(), 59 | true = erlang:register(?REG_NAME, VM), 60 | ok = schedule_vm_check(), 61 | {ok, #state{vm = VM}}. 62 | 63 | handle_call(_Msg, _From, State) -> 64 | lager:warning("rcvd unknown call msg: ~p from: ~p", [_Msg, _From]), 65 | {reply, ok, State}. 66 | 67 | handle_cast(_Msg, State) -> 68 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 69 | {noreply, State}. 70 | 71 | handle_info(?VM_CHECK_TICK, #state{vm = VM} = State) -> 72 | case erlang_v8:create_context(VM) of 73 | {ok, Context} -> 74 | case erlang_v8:eval(VM, Context, ?VM_CHECK_FUN) of 75 | {ok, _} -> 76 | case erlang_v8:call(VM, Context, ?VM_CHECK_FUN_NAME, [], 50) of 77 | {ok, 0} -> 78 | ok = erlang_v8:destroy_context(VM, Context), 79 | {noreply, State}; 80 | {error, _Reason} -> 81 | lager:error("failed to call function ~p", [_Reason]), 82 | {stop, failed_call_fun, State} 83 | end; 84 | {error, _Reason} -> 85 | lager:error("failed to eval function ~p", [_Reason]), 86 | {stop, failed_eval_fun, State} 87 | end; 88 | {error, _Reason} -> 89 | lager:error("failed to create context ~p", [_Reason]), 90 | {stop, failed_create_context, State} 91 | end; 92 | handle_info(_Msg, State) -> 93 | lager:warning("rcvd unknown info msg: ~p", [_Msg]), 94 | {noreply, State}. 95 | 96 | code_change(_OldVsn, State, _Extra) -> 97 | {ok, State}. 98 | 99 | terminate(_Reason, #state{vm = VM} = _State) -> 100 | _ = erlang:unregister(?REG_NAME), 101 | _ = erlang_v8:stop_vm(VM), 102 | ok. 103 | 104 | %% ------------------------------------------------------------------ 105 | %% Internal Function Definitions 106 | %% ------------------------------------------------------------------ 107 | 108 | -spec schedule_vm_check() -> ok. 109 | schedule_vm_check() -> 110 | {ok, _} = timer:send_interval(?VM_CHECK_TIMER, self(), ?VM_CHECK_TICK), 111 | ok. 112 | 113 | %% ------------------------------------------------------------------ 114 | %% EUNIT Tests 115 | %% ------------------------------------------------------------------ 116 | -ifdef(TEST). 117 | 118 | vm_check_test() -> 119 | {ok, Pid} = ?MODULE:start_link(#{}), 120 | Pid ! ?VM_CHECK_TICK, 121 | timer:sleep(100), 122 | 123 | ?assert(erlang:is_process_alive(Pid)), 124 | 125 | gen_server:stop(Pid), 126 | ok. 127 | 128 | -endif. 129 | -------------------------------------------------------------------------------- /src/device/router_device_cache.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @doc 3 | %%% == Router Device Cache == 4 | %%% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_device_cache). 7 | 8 | -include_lib("stdlib/include/ms_transform.hrl"). 9 | 10 | -include("router_device.hrl"). 11 | 12 | -define(ETS, router_device_cache_ets). 13 | -define(DEVADDR_ETS, router_device_cache_devaddr_ets). 14 | 15 | %% ------------------------------------------------------------------ 16 | %% API Exports 17 | %% ------------------------------------------------------------------ 18 | -export([ 19 | init/0, 20 | get/0, get/1, 21 | get_by_devaddr/1, 22 | save/1, save/2, 23 | delete/1, 24 | size/0 25 | ]). 26 | 27 | %% ------------------------------------------------------------------ 28 | %% API Functions 29 | %% ------------------------------------------------------------------ 30 | 31 | -spec init() -> ok. 32 | init() -> 33 | _ = ets:new(?ETS, [ 34 | public, 35 | named_table, 36 | set, 37 | {write_concurrency, true}, 38 | {read_concurrency, true} 39 | ]), 40 | _ = ets:new(?DEVADDR_ETS, [ 41 | public, 42 | named_table, 43 | set, 44 | {write_concurrency, true}, 45 | {read_concurrency, true} 46 | ]), 47 | ok = init_from_db(), 48 | ok. 49 | 50 | -spec get() -> [router_device:device()]. 51 | get() -> 52 | [Device || {_ID, Device} <- ets:tab2list(?ETS)]. 53 | 54 | -spec get(binary()) -> {ok, router_device:device()} | {error, not_found}. 55 | get(DeviceID) -> 56 | case ets:lookup(?ETS, DeviceID) of 57 | [] -> {error, not_found}; 58 | [{DeviceID, Device}] -> {ok, Device} 59 | end. 60 | 61 | -spec get_by_devaddr(binary()) -> [router_device:device()]. 62 | get_by_devaddr(DevAddr) -> 63 | case ets:lookup(?DEVADDR_ETS, DevAddr) of 64 | [] -> []; 65 | [{_, Ref}] -> ets:tab2list(Ref) 66 | end. 67 | 68 | all_devaddrs_tables() -> 69 | ets:tab2list(?DEVADDR_ETS). 70 | 71 | -spec save(router_device:device()) -> {ok, router_device:device()}. 72 | save(Device) -> 73 | save(Device, [ 74 | {monitor, [{tag, {'DOWN', device_save}}]} 75 | ]). 76 | 77 | -spec save(Device :: router_device:device(), Opts :: list()) -> {ok, router_device:device()}. 78 | save(Device, Opts) -> 79 | DeviceID = router_device:id(Device), 80 | true = ets:insert(?ETS, {DeviceID, Device}), 81 | _ = erlang:spawn_opt( 82 | fun() -> 83 | AddrsEtsMap = maps:from_list(all_devaddrs_tables()), 84 | CurrentDevaddrs = router_device:devaddrs(Device), 85 | 86 | %% Build a list of all DevAddrs we can know about. 87 | AllAddrs = lists:usort(maps:keys(AddrsEtsMap) ++ CurrentDevaddrs), 88 | 89 | lists:foreach( 90 | fun(Addr) -> 91 | EtsRef = 92 | case maps:get(Addr, AddrsEtsMap, undefined) of 93 | undefined -> make_devaddr_table(Addr); 94 | Ref -> Ref 95 | end, 96 | case lists:member(Addr, CurrentDevaddrs) of 97 | true -> ets:insert(EtsRef, Device); 98 | false -> ets:delete(EtsRef, DeviceID) 99 | end 100 | end, 101 | AllAddrs 102 | ) 103 | end, 104 | Opts 105 | ), 106 | {ok, Device}. 107 | 108 | -spec delete(binary()) -> ok. 109 | delete(DeviceID) -> 110 | true = ets:delete(?ETS, DeviceID), 111 | ok. 112 | 113 | -spec size() -> non_neg_integer(). 114 | size() -> 115 | proplists:get_value(size, ets:info(router_device_cache_ets), 0). 116 | 117 | %% ------------------------------------------------------------------ 118 | %% Internal Function Definitions 119 | %% ------------------------------------------------------------------ 120 | 121 | -spec make_devaddr_table(binary()) -> ets:tab(). 122 | make_devaddr_table(DevAddr) -> 123 | Ref = ets:new(devaddr_table, [ 124 | public, 125 | set, 126 | %% items are router_device:device() 127 | %% keypos is the id field of the record. 128 | {keypos, 2}, 129 | {heir, whereis(router_sup), DevAddr} 130 | ]), 131 | true = ets:insert(?DEVADDR_ETS, {DevAddr, Ref}), 132 | Ref. 133 | 134 | -spec init_from_db() -> ok. 135 | init_from_db() -> 136 | {ok, DB, [_DefaultCF, DevicesCF]} = router_db:get(), 137 | Devices = router_device:get(DB, DevicesCF), 138 | %% TODO: improve this maybe? 139 | lists:foreach(fun(Device) -> ?MODULE:save(Device) end, Devices). 140 | 141 | %% ------------------------------------------------------------------ 142 | %% EUNIT Tests 143 | %% ------------------------------------------------------------------ 144 | -ifdef(TEST). 145 | 146 | -include_lib("eunit/include/eunit.hrl"). 147 | 148 | init_from_db_test() -> 149 | Dir = test_utils:tmp_dir("init_from_db_test"), 150 | {ok, Pid} = router_db:start_link([Dir]), 151 | ok = init(), 152 | ID = router_utils:uuid_v4(), 153 | Device0 = router_device:new(ID), 154 | DevAddr0 = <<"devaddr0">>, 155 | Updates = [ 156 | {name, <<"name">>}, 157 | {app_eui, <<"app_eui">>}, 158 | {dev_eui, <<"dev_eui">>}, 159 | {keys, [{<<"nwk_s_key">>, <<"app_s_key">>}]}, 160 | {devaddrs, [DevAddr0]}, 161 | {dev_nonces, [<<"1">>]}, 162 | {fcnt, 1}, 163 | {fcntdown, 1}, 164 | {offset, 1}, 165 | {channel_correction, true}, 166 | {queue, [a]}, 167 | {region, 'US915'}, 168 | {last_known_datarate, 7}, 169 | {ecc_compact, #{}}, 170 | {location, <<"location">>}, 171 | {metadata, #{a => b}}, 172 | {is_active, false} 173 | ], 174 | Device1 = router_device:update(Updates, Device0), 175 | {ok, DB, [_, CF]} = router_db:get(), 176 | ?assertEqual({ok, Device1}, router_device:save(DB, CF, Device1)), 177 | ?assertEqual(ok, init_from_db()), 178 | ?assertEqual({ok, Device1}, ?MODULE:get(ID)), 179 | timer:sleep(10), 180 | ?assertEqual([Device1], ?MODULE:get_by_devaddr(DevAddr0)), 181 | 182 | DevAddr1 = <<"devaddr1">>, 183 | DevAddr2 = <<"devaddr2">>, 184 | Device2 = router_device:devaddrs([DevAddr1, DevAddr2], Device1), 185 | ?assertEqual({ok, Device2}, ?MODULE:save(Device2)), 186 | timer:sleep(10), 187 | ?assertEqual([], ?MODULE:get_by_devaddr(DevAddr0)), 188 | ?assertEqual([Device2], ?MODULE:get_by_devaddr(DevAddr1)), 189 | ?assertEqual([Device2], ?MODULE:get_by_devaddr(DevAddr2)), 190 | 191 | gen_server:stop(Pid), 192 | ets:delete(?ETS), 193 | ets:delete(?DEVADDR_ETS), 194 | ok. 195 | 196 | get_save_delete_test() -> 197 | Dir = test_utils:tmp_dir("get_save_delete_test"), 198 | {ok, Pid} = router_db:start_link([Dir]), 199 | ok = init(), 200 | ID = router_utils:uuid_v4(), 201 | Device = router_device:new(ID), 202 | 203 | ?assertEqual({ok, Device}, ?MODULE:save(Device)), 204 | ?assertEqual({ok, Device}, ?MODULE:get(ID)), 205 | ?assertEqual([Device], ?MODULE:get()), 206 | ?assertEqual(ok, ?MODULE:delete(ID)), 207 | ?assertEqual({error, not_found}, ?MODULE:get(ID)), 208 | 209 | gen_server:stop(Pid), 210 | ets:delete(?ETS), 211 | ets:delete(?DEVADDR_ETS), 212 | ok. 213 | 214 | -endif. 215 | -------------------------------------------------------------------------------- /src/device/router_devices_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @doc 3 | %%% == Router Devices Supervisor == 4 | %%% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_devices_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% ------------------------------------------------------------------ 11 | %% API Exports 12 | %% ------------------------------------------------------------------ 13 | -export([ 14 | start_link/0, 15 | maybe_start_worker/2, 16 | lookup_device_worker/1, 17 | id/1 18 | ]). 19 | 20 | %% ------------------------------------------------------------------ 21 | %% Supervisor Exports 22 | %% ------------------------------------------------------------------ 23 | -export([init/1]). 24 | 25 | -define(WORKER(I), #{ 26 | id => I, 27 | start => {I, start_link, []}, 28 | restart => temporary, 29 | shutdown => 1000, 30 | type => worker, 31 | modules => [I] 32 | }). 33 | 34 | -define(FLAGS, #{ 35 | strategy => simple_one_for_one, 36 | intensity => 3, 37 | period => 60 38 | }). 39 | 40 | -define(ETS, router_devices_ets). 41 | 42 | %% ------------------------------------------------------------------ 43 | %% API functions 44 | %% ------------------------------------------------------------------ 45 | 46 | start_link() -> 47 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 48 | 49 | -spec maybe_start_worker(binary(), map()) -> {ok, pid()} | {error, any()}. 50 | maybe_start_worker(ID, Args) -> 51 | case ets:lookup(?ETS, ID) of 52 | [] -> 53 | start_worker(ID, Args); 54 | [{ID, Pid}] -> 55 | case erlang:is_process_alive(Pid) of 56 | true -> 57 | {ok, Pid}; 58 | false -> 59 | _ = ets:delete(?ETS, ID), 60 | start_worker(ID, Args) 61 | end 62 | end. 63 | 64 | -spec lookup_device_worker(binary()) -> {ok, pid()} | {error, not_found}. 65 | lookup_device_worker(ID) -> 66 | case ets:lookup(?ETS, ID) of 67 | [] -> 68 | {error, not_found}; 69 | [{ID, Pid}] -> 70 | case erlang:is_process_alive(Pid) of 71 | true -> {ok, Pid}; 72 | false -> {error, not_found} 73 | end 74 | end. 75 | 76 | -spec id(binary()) -> binary(). 77 | id(DeviceId) -> 78 | DeviceId. 79 | 80 | %% ------------------------------------------------------------------ 81 | %% Supervisor callbacks 82 | %% ------------------------------------------------------------------ 83 | 84 | init([]) -> 85 | ets:new(?ETS, [public, named_table, set]), 86 | ok = router_device_multibuy:init(), 87 | lager:info("router_device_multibuy:init"), 88 | ok = router_device_routing:init(), 89 | lager:info("router_device_routing:init"), 90 | ok = router_device_cache:init(), 91 | lager:info("router_device_cache:init"), 92 | {ok, {?FLAGS, [?WORKER(router_device_worker)]}}. 93 | 94 | %% ------------------------------------------------------------------ 95 | %% Internal Function Definitions 96 | %% ------------------------------------------------------------------ 97 | 98 | -spec start_worker(binary(), map()) -> {ok, pid()} | {error, any()}. 99 | start_worker(ID, Args) -> 100 | {ok, DB, [_DefaultCF, DevicesCF]} = router_db:get(), 101 | Map = maps:merge(Args, #{db => DB, cf => DevicesCF, id => ID}), 102 | case supervisor:start_child(?MODULE, [Map]) of 103 | {error, _Err} = Err -> 104 | Err; 105 | {ok, Pid} = OK -> 106 | case ets:insert_new(?ETS, {ID, Pid}) of 107 | true -> 108 | OK; 109 | false -> 110 | supervisor:terminate_child(?MODULE, Pid), 111 | maybe_start_worker(ID, Args) 112 | end 113 | end. 114 | -------------------------------------------------------------------------------- /src/grpc/helium_packet_service.erl: -------------------------------------------------------------------------------- 1 | -module(helium_packet_service). 2 | 3 | -behavior(helium_packet_router_packet_bhvr). 4 | 5 | -include("./autogen/packet_router_pb.hrl"). 6 | -include_lib("helium_proto/include/packet_pb.hrl"). 7 | 8 | -define(JOIN_REQUEST, 2#000). 9 | 10 | -export([ 11 | init/2, 12 | route/2, 13 | handle_info/2 14 | ]). 15 | 16 | -spec init(atom(), grpcbox_stream:t()) -> grpcbox_stream:t(). 17 | init(_Rpc, Stream) -> 18 | Stream. 19 | 20 | -spec route(packet_router_pb:envelope_up_v1_pb(), grpcbox_stream:t()) -> 21 | {ok, grpcbox_stream:t()} | grpcbox_stream:grpc_error_response(). 22 | route(eos, StreamState) -> 23 | lager:debug("got eos"), 24 | {stop, StreamState}; 25 | route(#envelope_up_v1_pb{data = {packet, PacketUp}}, StreamState) -> 26 | Self = self(), 27 | erlang:spawn(fun() -> 28 | SCPacket = to_sc_packet(PacketUp), 29 | router_device_routing:handle_free_packet( 30 | SCPacket, erlang:system_time(millisecond), Self 31 | ) 32 | end), 33 | {ok, StreamState}; 34 | route(_EnvUp, StreamState) -> 35 | lager:warning("unknown ~p", [_EnvUp]), 36 | {ok, StreamState}. 37 | 38 | -spec handle_info(Msg :: any(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). 39 | handle_info( 40 | {send_purchase, _PurchaseSC, Hotspot, _PacketHash, _Region, _OwnerSigFun}, StreamState 41 | ) -> 42 | GatewayName = blockchain_utils:addr2name(Hotspot), 43 | lager:debug("ignoring send_purchase to ~s ~p", [GatewayName, StreamState]), 44 | StreamState; 45 | handle_info({send_response, Reply}, StreamState) -> 46 | lager:debug("send_response ~p", [Reply]), 47 | case from_sc_packet(Reply) of 48 | ignore -> 49 | StreamState; 50 | EnvDown -> 51 | lager:debug("send EnvDown ~p", [EnvDown]), 52 | grpcbox_stream:send(false, EnvDown, StreamState) 53 | end; 54 | handle_info(_Msg, StreamState) -> 55 | %% NOTE: For testing non-reply flows 56 | case application:get_env(router, packet_router_grpc_forward_unhandled_messages, undefined) of 57 | {Pid, Atom} when erlang:is_pid(Pid) andalso erlang:is_atom(Atom) -> Pid ! {Atom, _Msg}; 58 | _ -> ok 59 | end, 60 | lager:debug("got an unhandled message ~p", [_Msg]), 61 | StreamState. 62 | 63 | %% ------------------------------------------------------------------ 64 | %% Helper Functions 65 | %% ------------------------------------------------------------------ 66 | 67 | -spec to_sc_packet(packet_router_pb:packet_router_packet_up_v1_pb()) -> 68 | router_pb:blockchain_state_channel_packet_v1_pb(). 69 | to_sc_packet(HprPacketUp) -> 70 | % Decompose uplink message 71 | #packet_router_packet_up_v1_pb{ 72 | % signature = Signature 73 | payload = Payload, 74 | timestamp = Timestamp, 75 | rssi = SignalStrength, 76 | %% This is coming in as hz 77 | frequency = Frequency, 78 | datarate = DataRate, 79 | snr = SNR, 80 | region = Region, 81 | hold_time = HoldTime, 82 | gateway = Gateway 83 | } = HprPacketUp, 84 | 85 | Packet = blockchain_helium_packet_v1:new( 86 | lorawan, 87 | Payload, 88 | Timestamp, 89 | erlang:float(SignalStrength), 90 | %% hz to Mhz 91 | Frequency / 1000000, 92 | erlang:atom_to_list(DataRate), 93 | SNR, 94 | routing_information(Payload) 95 | ), 96 | blockchain_state_channel_packet_v1:new(Packet, Gateway, Region, HoldTime). 97 | 98 | -spec routing_information(binary()) -> 99 | {devaddr, DevAddr :: non_neg_integer()} 100 | | {eui, DevEUI :: non_neg_integer(), AppEUI :: non_neg_integer()}. 101 | routing_information( 102 | <> 104 | ) -> 105 | {eui, DevEUI, AppEUI}; 106 | routing_information(<<_FType:3, _:5, DevAddr:32/integer-unsigned-little, _/binary>>) -> 107 | % routing_information_pb{data = {devaddr, DevAddr}}. 108 | {devaddr, DevAddr}. 109 | 110 | %% =================================================================== 111 | 112 | -spec from_sc_packet(router_pb:blockchain_state_channel_response_v1_pb()) -> 113 | packet_router_db:envelope_down_v1_pb() | ignore. 114 | from_sc_packet(StateChannelResponse) -> 115 | case blockchain_state_channel_response_v1:downlink(StateChannelResponse) of 116 | undefined -> 117 | ignore; 118 | Downlink -> 119 | PacketDown = #packet_router_packet_down_v1_pb{ 120 | payload = blockchain_helium_packet_v1:payload(Downlink), 121 | rx1 = #window_v1_pb{ 122 | timestamp = blockchain_helium_packet_v1:timestamp(Downlink), 123 | %% Mhz to hz 124 | frequency = erlang:round( 125 | blockchain_helium_packet_v1:frequency(Downlink) * 1_000_000 126 | ), 127 | datarate = hpr_datarate(blockchain_helium_packet_v1:datarate(Downlink)) 128 | }, 129 | rx2 = rx2_window(blockchain_helium_packet_v1:rx2_window(Downlink)) 130 | }, 131 | #envelope_down_v1_pb{data = {packet, PacketDown}} 132 | end. 133 | 134 | -spec hpr_datarate(unicode:chardata()) -> 135 | packet_router_pb:'helium.data_rate'(). 136 | hpr_datarate(DataRateString) -> 137 | erlang:binary_to_existing_atom(unicode:characters_to_binary(DataRateString)). 138 | 139 | -spec rx2_window(blockchain_helium_packet_v1:window()) -> 140 | undefined | packet_router_pb:window_v1_pb(). 141 | rx2_window(#window_pb{timestamp = RX2Timestamp, frequency = RX2Frequency, datarate = RX2Datarate}) -> 142 | #window_v1_pb{ 143 | timestamp = RX2Timestamp, 144 | %% Mhz to hz 145 | frequency = erlang:round(RX2Frequency * 1_000_000), 146 | datarate = hpr_datarate(RX2Datarate) 147 | }; 148 | rx2_window(undefined) -> 149 | undefined. 150 | -------------------------------------------------------------------------------- /src/grpc/helium_router_service.erl: -------------------------------------------------------------------------------- 1 | -module(helium_router_service). 2 | 3 | -behavior(helium_router_bhvr). 4 | 5 | -include_lib("helium_proto/include/blockchain_state_channel_v1_pb.hrl"). 6 | 7 | -ifdef(TEST). 8 | -define(TIMEOUT, 5000). 9 | -else. 10 | -define(TIMEOUT, 8000). 11 | -endif. 12 | 13 | -export([ 14 | route/2 15 | ]). 16 | 17 | route(Ctx, #blockchain_state_channel_message_v1_pb{msg = {packet, SCPacket}} = _Message) -> 18 | lager:debug("executing RPC route with msg ~p", [_Message]), 19 | 20 | BasePacket = SCPacket#blockchain_state_channel_packet_v1_pb{signature = <<>>}, 21 | EncodedPacket = blockchain_state_channel_packet_v1:encode(BasePacket), 22 | Signature = blockchain_state_channel_packet_v1:signature(SCPacket), 23 | PubKeyBin = blockchain_state_channel_packet_v1:hotspot(SCPacket), 24 | PubKey = libp2p_crypto:bin_to_pubkey(PubKeyBin), 25 | case libp2p_crypto:verify(EncodedPacket, Signature, PubKey) of 26 | false -> 27 | {grpc_error, {grpcbox_stream:code_to_status(2), <<"bad signature">>}}; 28 | true -> 29 | %% handle the packet and then await a response 30 | %% if no response within given time, then give up and return error 31 | router_device_routing:handle_free_packet( 32 | SCPacket, erlang:system_time(millisecond), self() 33 | ), 34 | wait_for_response(Ctx) 35 | end. 36 | 37 | %% ------------------------------------------------------------------ 38 | %% Internal functions 39 | %% ------------------------------------------------------------------ 40 | wait_for_response(Ctx) -> 41 | receive 42 | {send_response, Resp} -> 43 | lager:debug("received response msg ~p", [Resp]), 44 | {ok, #blockchain_state_channel_message_v1_pb{msg = {response, Resp}}, Ctx}; 45 | {packet, Packet} -> 46 | lager:debug("received packet ~p", [Packet]), 47 | {ok, #blockchain_state_channel_message_v1_pb{msg = {packet, Packet}}, Ctx}; 48 | {error, Reason} -> 49 | lager:debug("received error msg ~p", [Reason]), 50 | {grpc_error, {grpcbox_stream:code_to_status(2), erlang:atom_to_binary(Reason)}} 51 | after ?TIMEOUT -> 52 | lager:debug("failed to receive response msg after ~p seconds", [?TIMEOUT]), 53 | {grpc_error, {grpcbox_stream:code_to_status(2), <<"timeout no response">>}} 54 | end. 55 | -------------------------------------------------------------------------------- /src/grpc/router_cli_migration_skf_list_handler.erl: -------------------------------------------------------------------------------- 1 | -module(router_cli_migration_skf_list_handler). 2 | 3 | -behaviour(grpcbox_client_stream). 4 | 5 | -include("./autogen/iot_config_pb.hrl"). 6 | 7 | -export([ 8 | init/3, 9 | handle_message/2, 10 | handle_headers/2, 11 | handle_trailers/4, 12 | handle_eos/1 13 | ]). 14 | 15 | -record(state, { 16 | options :: map(), 17 | data :: list(iot_config_pb:iot_config_session_key_filter_v1_pb()) 18 | }). 19 | 20 | -type stream_id() :: non_neg_integer(). 21 | -type state() :: #state{}. 22 | 23 | -spec init(pid(), stream_id(), Options :: map() | undefined) -> {ok, state()}. 24 | init(_ConnectionPid, _StreamId, Options) -> 25 | lager:info("init ~p: ~p", [_StreamId, Options]), 26 | {ok, #state{options = Options, data = []}}. 27 | 28 | -spec handle_message(iot_config_pb:iot_config_session_key_filter_v1_pb(), state()) -> {ok, state()}. 29 | handle_message(SKF, #state{data = Data} = State) -> 30 | lager:debug("got ~p", [SKF]), 31 | {ok, State#state{data = [SKF | Data]}}. 32 | 33 | -spec handle_headers(map(), state()) -> {ok, state()}. 34 | handle_headers(_Metadata, CBData) -> 35 | {ok, CBData}. 36 | 37 | -spec handle_trailers(binary(), term(), map(), state()) -> {ok, state()}. 38 | handle_trailers(_Status, _Message, _Metadata, CBData) -> 39 | {ok, CBData}. 40 | 41 | -spec handle_eos(state()) -> {ok, state()}. 42 | handle_eos(#state{options = Options, data = Data} = State) -> 43 | lager:info("got eos, sending to router_ics_skf_worker"), 44 | maps:get(pid, Options) ! {?MODULE, Data}, 45 | {ok, State}. 46 | -------------------------------------------------------------------------------- /src/grpc/router_grpc_client_worker.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | -module(router_grpc_client_worker). 6 | 7 | -behavior(gen_server). 8 | 9 | %% ------------------------------------------------------------------ 10 | %% API Function Exports 11 | %% ------------------------------------------------------------------ 12 | -export([ 13 | start_link/0 14 | ]). 15 | 16 | %% ------------------------------------------------------------------ 17 | %% gen_server Function Exports 18 | %% ------------------------------------------------------------------ 19 | -export([ 20 | init/1, 21 | handle_call/3, 22 | handle_cast/2, 23 | handle_info/2, 24 | terminate/2, 25 | code_change/3 26 | ]). 27 | 28 | -define(SERVER, ?MODULE). 29 | 30 | -record(state, {}). 31 | 32 | %% ------------------------------------------------------------------ 33 | %% API Function Definitions 34 | %% ------------------------------------------------------------------ 35 | start_link() -> 36 | gen_server:start_link({local, ?SERVER}, ?SERVER, #{}, []). 37 | 38 | %% ------------------------------------------------------------------ 39 | %% gen_server Function Definitions 40 | %% ------------------------------------------------------------------ 41 | init(_Args) -> 42 | lager:info("~p init with ~p", [?SERVER, _Args]), 43 | case application:get_env(grpcbox, client) of 44 | {ok, #{channels := Channels}} -> 45 | lists:foreach( 46 | fun({Name, Endpoints, Options}) -> 47 | R = grpcbox_channel_sup:start_child(Name, Endpoints, Options), 48 | lager:info("started ~p ~p", [{Name, Endpoints, Options}, R]) 49 | end, 50 | Channels 51 | ); 52 | _ -> 53 | ok 54 | end, 55 | {ok, #state{}}. 56 | 57 | handle_call(_Msg, _From, State) -> 58 | lager:warning("rcvd unknown call msg: ~p from: ~p", [_Msg, _From]), 59 | {reply, ok, State}. 60 | 61 | handle_cast(_Msg, State) -> 62 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 63 | {noreply, State}. 64 | 65 | handle_info(_Msg, State) -> 66 | lager:warning("rcvd unknown info msg: ~p", [_Msg]), 67 | {noreply, State}. 68 | 69 | code_change(_OldVsn, State, _Extra) -> 70 | {ok, State}. 71 | 72 | terminate(_Reason, #state{}) -> 73 | ok. 74 | -------------------------------------------------------------------------------- /src/grpc/router_grpc_server_worker.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | -module(router_grpc_server_worker). 6 | 7 | -behavior(gen_server). 8 | 9 | %% ------------------------------------------------------------------ 10 | %% API Function Exports 11 | %% ------------------------------------------------------------------ 12 | -export([ 13 | start_link/0 14 | ]). 15 | 16 | %% ------------------------------------------------------------------ 17 | %% gen_server Function Exports 18 | %% ------------------------------------------------------------------ 19 | -export([ 20 | init/1, 21 | handle_call/3, 22 | handle_cast/2, 23 | handle_info/2, 24 | terminate/2, 25 | code_change/3 26 | ]). 27 | 28 | -define(SERVER, ?MODULE). 29 | 30 | -record(state, {}). 31 | 32 | %% ------------------------------------------------------------------ 33 | %% API Function Definitions 34 | %% ------------------------------------------------------------------ 35 | start_link() -> 36 | gen_server:start_link({local, ?SERVER}, ?SERVER, #{}, []). 37 | 38 | %% ------------------------------------------------------------------ 39 | %% gen_server Function Definitions 40 | %% ------------------------------------------------------------------ 41 | init(_Args) -> 42 | lager:info("~p init with ~p", [?SERVER, _Args]), 43 | lists:foreach( 44 | fun(ServerOpts) -> 45 | R = grpcbox_services_simple_sup:start_child(ServerOpts), 46 | lager:info("started ~p ~p", [ServerOpts, R]) 47 | end, 48 | application:get_env(grpcbox, servers, []) 49 | ), 50 | {ok, #state{}}. 51 | 52 | handle_call(_Msg, _From, State) -> 53 | lager:warning("rcvd unknown call msg: ~p from: ~p", [_Msg, _From]), 54 | {reply, ok, State}. 55 | 56 | handle_cast(_Msg, State) -> 57 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 58 | {noreply, State}. 59 | 60 | handle_info(_Msg, State) -> 61 | lager:warning("rcvd unknown info msg: ~p", [_Msg]), 62 | {noreply, State}. 63 | 64 | code_change(_OldVsn, State, _Extra) -> 65 | {ok, State}. 66 | 67 | terminate(_Reason, #state{}) -> 68 | ok. 69 | 70 | %% ------------------------------------------------------------------ 71 | %% Internal Function Definitions 72 | %% ------------------------------------------------------------------ 73 | -------------------------------------------------------------------------------- /src/grpc/router_ics_gateway_location_worker.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router IOT Config Service Gateway Location Worker == 4 | %% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_ics_gateway_location_worker). 7 | 8 | -include("./autogen/iot_config_pb.hrl"). 9 | 10 | -export([ 11 | init_ets/0, 12 | get/1 13 | ]). 14 | 15 | -define(ETS, router_ics_gateway_location_worker_ets). 16 | -ifdef(TEST). 17 | -define(BACKOFF_MIN, 100). 18 | -else. 19 | -define(BACKOFF_MIN, timer:seconds(10)). 20 | -endif. 21 | -define(BACKOFF_MAX, timer:minutes(5)). 22 | -define(CACHED_NOT_FOUND, cached_not_found). 23 | 24 | -record(location, { 25 | gateway :: libp2p_crypto:pubkey_bin(), 26 | timestamp :: non_neg_integer(), 27 | h3_index :: h3:index() | undefined 28 | }). 29 | 30 | -spec init_ets() -> ok. 31 | init_ets() -> 32 | ?ETS = ets:new(?ETS, [ 33 | public, 34 | named_table, 35 | set, 36 | {read_concurrency, true}, 37 | {keypos, #location.gateway} 38 | ]), 39 | ok. 40 | 41 | -spec get(libp2p_crypto:pubkey_bin()) -> {ok, h3:index()} | {error, any()}. 42 | get(PubKeyBin) -> 43 | case router_utils:get_env_bool(bypass_location_lookup, false) of 44 | true -> 45 | {error, not_found}; 46 | false -> 47 | case lookup(PubKeyBin) of 48 | {error, ?CACHED_NOT_FOUND} = E -> 49 | E; 50 | {error, _Reason} -> 51 | HotspotName = blockchain_utils:addr2name(PubKeyBin), 52 | case get_gateway_location(PubKeyBin) of 53 | {error, ErrReason, _} -> 54 | lager:warning( 55 | "fail to get_gateway_location ~p for ~s", 56 | [ErrReason, HotspotName] 57 | ), 58 | ok = insert(PubKeyBin, ?CACHED_NOT_FOUND), 59 | {error, ErrReason}; 60 | {ok, H3IndexString} -> 61 | H3Index = h3:from_string(H3IndexString), 62 | ok = insert(PubKeyBin, H3Index), 63 | {ok, H3Index} 64 | end; 65 | {ok, _} = OK -> 66 | OK 67 | end 68 | end. 69 | 70 | %% ------------------------------------------------------------------ 71 | %% Internal Function Definitions 72 | %% ------------------------------------------------------------------ 73 | 74 | %% Store valid locations for up to 24 hours. 75 | %% Invalid locations for 1 hour. 76 | -spec lookup(PubKeyBin :: libp2p_crypto:pubkey_bin()) -> 77 | {ok, h3:index()} | {error, ?CACHED_NOT_FOUND | not_found | outdated}. 78 | lookup(PubKeyBin) -> 79 | Yesterday = erlang:system_time(millisecond) - timer:hours(24), 80 | OneHour = erlang:system_time(millisecond) - timer:hours(1), 81 | case ets:lookup(?ETS, PubKeyBin) of 82 | [] -> 83 | {error, not_found}; 84 | [#location{timestamp = T}] when T < Yesterday -> 85 | {error, outdated}; 86 | [#location{timestamp = T, h3_index = ?CACHED_NOT_FOUND}] when T < OneHour -> 87 | {error, outdated}; 88 | [#location{h3_index = ?CACHED_NOT_FOUND}] -> 89 | {error, ?CACHED_NOT_FOUND}; 90 | [#location{h3_index = H3Index}] -> 91 | {ok, H3Index} 92 | end. 93 | 94 | -spec insert(PubKeyBin :: libp2p_crypto:pubkey_bin(), H3Index :: h3:index()) -> ok. 95 | insert(PubKeyBin, H3Index) -> 96 | true = ets:insert(?ETS, #location{ 97 | gateway = PubKeyBin, 98 | timestamp = erlang:system_time(millisecond), 99 | h3_index = H3Index 100 | }), 101 | ok. 102 | 103 | %% We have to do this because the call to `helium_iot_config_gateway_client:location` can return 104 | %% `{error, {Status, Reason}, _}` but is not in the spec... 105 | -dialyzer({nowarn_function, get_gateway_location/1}). 106 | 107 | -spec get_gateway_location(PubKeyBin :: libp2p_crypto:pubkey_bin()) -> 108 | {ok, string()} | {error, any(), boolean()}. 109 | get_gateway_location(PubKeyBin) -> 110 | SigFun = router_blockchain:sig_fun(), 111 | Req = #iot_config_gateway_location_req_v1_pb{ 112 | gateway = PubKeyBin, 113 | signer = router_blockchain:pubkey_bin() 114 | }, 115 | EncodedReq = iot_config_pb:encode_msg(Req, iot_config_gateway_location_req_v1_pb), 116 | SignedReq = Req#iot_config_gateway_location_req_v1_pb{signature = SigFun(EncodedReq)}, 117 | case 118 | helium_iot_config_gateway_client:location(SignedReq, #{ 119 | channel => router_ics_utils:location_channel() 120 | }) 121 | of 122 | {error, {Status, Reason}, _} when is_binary(Status) -> 123 | {error, {grpcbox_utils:status_to_string(Status), Reason}, false}; 124 | {grpc_error, Reason} -> 125 | {error, Reason, false}; 126 | {error, Reason} -> 127 | {error, Reason, true}; 128 | {ok, #iot_config_gateway_location_res_v1_pb{location = Location}, _Meta} -> 129 | {ok, Location} 130 | end. 131 | -------------------------------------------------------------------------------- /src/grpc/router_ics_route_get_devaddrs_handler.erl: -------------------------------------------------------------------------------- 1 | -module(router_ics_route_get_devaddrs_handler). 2 | 3 | -behaviour(grpcbox_client_stream). 4 | 5 | %% -include("./autogen/iot_config_pb.hrl"). 6 | 7 | -export([ 8 | init/3, 9 | handle_message/2, 10 | handle_headers/2, 11 | handle_trailers/4, 12 | handle_eos/1 13 | ]). 14 | 15 | -record(state, { 16 | data :: list(iot_config_pb:iot_config_devaddr_range_v1_pb()), 17 | error :: undefined | {error, any()} 18 | }). 19 | 20 | -type stream_id() :: non_neg_integer(). 21 | -type state() :: #state{}. 22 | 23 | -spec init(pid(), stream_id(), any()) -> {ok, state()}. 24 | init(_ConnectionPid, _StreamId, _Any) -> 25 | lager:info("init ~p: ~p", [_StreamId, _Any]), 26 | {ok, #state{data = [], error = undefined}}. 27 | 28 | -spec handle_message(iot_config_pb:iot_config_devaddr_range_v1_pb(), state()) -> {ok, state()}. 29 | handle_message(DevaddrRange, #state{data = Data} = State) -> 30 | lager:info("got ~p", [DevaddrRange]), 31 | {ok, State#state{data = [DevaddrRange | Data]}}. 32 | 33 | -spec handle_headers(map(), state()) -> {ok, state()}. 34 | handle_headers(_Metadata, CBData) -> 35 | {ok, CBData}. 36 | 37 | handle_trailers(Status, Message, Metadata, #state{} = State) -> 38 | lager:info("trailers: [status: ~p] [message: ~p] [meta: ~p]", [Status, Message, Metadata]), 39 | case Status of 40 | <<"0">> -> 41 | {ok, State}; 42 | _ -> 43 | lager:error("trailers ~p", [{error, {Status, Message, Metadata}}]), 44 | {ok, State#state{error = {error, Message}}} 45 | end. 46 | 47 | -spec handle_eos(state()) -> {ok, state()}. 48 | handle_eos(#state{data = Data, error = undefined} = State) -> 49 | lager:info("got eos, sending to router_device_devaddr"), 50 | ok = router_device_devaddr:reconcile_end({ok, Data}), 51 | {ok, State}; 52 | handle_eos(#state{error = Error} = State) -> 53 | lager:warning("got eos with error ~p", [Error]), 54 | ok = router_device_devaddr:reconcile_end(Error), 55 | {ok, State}. 56 | -------------------------------------------------------------------------------- /src/grpc/router_ics_route_get_euis_handler.erl: -------------------------------------------------------------------------------- 1 | -module(router_ics_route_get_euis_handler). 2 | 3 | -behaviour(grpcbox_client_stream). 4 | 5 | -export([ 6 | init/3, 7 | handle_message/2, 8 | handle_headers/2, 9 | handle_trailers/4, 10 | handle_eos/1 11 | ]). 12 | 13 | -record(state, { 14 | options :: map(), 15 | euis :: list(iot_config_pb:iot_config_eui_pair_v1_pb()) 16 | }). 17 | 18 | -type stream_id() :: non_neg_integer(). 19 | -type state() :: #state{}. 20 | 21 | -spec init(pid(), stream_id(), Options :: map()) -> {ok, state()}. 22 | init(_ConnectionPid, _StreamId, Options) -> 23 | lager:debug("init ~p: ~p", [_StreamId, Options]), 24 | {ok, #state{options = Options, euis = []}}. 25 | 26 | -spec handle_message(iot_config_pb:iot_config_eui_pair_v1_pb(), state()) -> {ok, state()}. 27 | handle_message(EUIPair, #state{euis = EUIPairs} = State) -> 28 | lager:debug("got ~p", [EUIPair]), 29 | {ok, State#state{euis = [EUIPair | EUIPairs]}}. 30 | 31 | -spec handle_headers(map(), state()) -> {ok, state()}. 32 | handle_headers(_Metadata, CBData) -> 33 | {ok, CBData}. 34 | 35 | -spec handle_trailers(binary(), term(), map(), state()) -> {ok, state()}. 36 | handle_trailers(_Status, _Message, _Metadata, CBData) -> 37 | {ok, CBData}. 38 | 39 | -spec handle_eos(state()) -> {ok, state()}. 40 | handle_eos(#state{options = Options, euis = EUIPairs} = State) -> 41 | lager:info("got eos, sending to router_ics_eui_worker"), 42 | ok = router_ics_eui_worker:reconcile_end(Options, EUIPairs), 43 | {ok, State}. 44 | -------------------------------------------------------------------------------- /src/grpc/router_ics_skf_list_handler.erl: -------------------------------------------------------------------------------- 1 | -module(router_ics_skf_list_handler). 2 | 3 | -behaviour(grpcbox_client_stream). 4 | 5 | -include("./autogen/iot_config_pb.hrl"). 6 | 7 | -export([ 8 | init/3, 9 | handle_message/2, 10 | handle_headers/2, 11 | handle_trailers/4, 12 | handle_eos/1 13 | ]). 14 | 15 | -record(state, { 16 | options :: map(), 17 | data :: list(iot_config_pb:iot_config_session_key_filter_v1_pb()) | {error, any()} 18 | }). 19 | 20 | -type stream_id() :: non_neg_integer(). 21 | -type state() :: #state{}. 22 | 23 | -spec init(pid(), stream_id(), Options :: map() | undefined) -> {ok, state()}. 24 | init(_ConnectionPid, _StreamId, Options) -> 25 | lager:info("init ~p: ~p", [_StreamId, Options]), 26 | {ok, #state{options = Options, data = []}}. 27 | 28 | -spec handle_message(iot_config_pb:iot_config_session_key_filter_v1_pb(), state()) -> {ok, state()}. 29 | handle_message(SKF, #state{data = Data} = State) -> 30 | lager:debug("got ~p", [SKF]), 31 | {ok, State#state{data = [SKF | Data]}}. 32 | 33 | -spec handle_headers(map(), state()) -> {ok, state()}. 34 | handle_headers(_Metadata, CBData) -> 35 | {ok, CBData}. 36 | 37 | -spec handle_trailers(binary(), term(), map(), state()) -> {ok, state()}. 38 | handle_trailers(Status, Message, Metadata, CBData0) -> 39 | CBData1 = 40 | case Status of 41 | <<"0">> -> CBData0; 42 | _ -> CBData0#state{data = {error, {Status, Message, Metadata}}} 43 | end, 44 | {ok, CBData1}. 45 | 46 | -spec handle_eos(state()) -> {ok, state()}. 47 | handle_eos(#state{options = Options, data = Data} = State) -> 48 | lager:info("got eos, sending to router_ics_skf_worker"), 49 | Callback = maps:get(callback, Options), 50 | case Data of 51 | {error, _} -> Callback(Data); 52 | _ -> Callback({ok, Data}) 53 | end, 54 | {ok, State}. 55 | -------------------------------------------------------------------------------- /src/grpc/router_ics_utils.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router IOT Config Service Worker == 4 | %% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_ics_utils). 7 | 8 | -export([ 9 | start_link_args/1, 10 | channel/0, 11 | location_channel/0, 12 | connect/3, 13 | batch_update/3 14 | ]). 15 | 16 | -define(ICS_CHANNEL, ics_channel). 17 | -define(ICS_LOCATION_CHANNEL, ics_location_channel). 18 | 19 | -spec start_link_args(map()) -> ignore | map(). 20 | start_link_args(#{transport := ""}) -> 21 | ignore; 22 | start_link_args(#{host := ""}) -> 23 | ignore; 24 | start_link_args(#{port := ""}) -> 25 | ignore; 26 | start_link_args(#{transport := "http"} = Args) -> 27 | start_link_args(Args#{transport => http}); 28 | start_link_args(#{transport := "https"} = Args) -> 29 | start_link_args(Args#{transport => https}); 30 | start_link_args(#{port := Port} = Args) when is_list(Port) -> 31 | start_link_args(Args#{port => erlang:list_to_integer(Port)}); 32 | start_link_args(#{transport := Transport, host := Host, port := Port} = Args) when 33 | is_atom(Transport) andalso is_list(Host) andalso is_integer(Port) 34 | -> 35 | Args; 36 | start_link_args(_) -> 37 | ignore. 38 | 39 | channel() -> 40 | ?ICS_CHANNEL. 41 | 42 | location_channel() -> 43 | ?ICS_LOCATION_CHANNEL. 44 | 45 | -spec connect(Transport :: http | https, Host :: string(), Port :: non_neg_integer()) -> 46 | ok | {error, any()}. 47 | connect(Transport, Host, Port) -> 48 | case grpcbox_channel:pick(?MODULE:channel(), stream) of 49 | {error, _} -> 50 | case 51 | grpcbox_client:connect(?MODULE:channel(), [{Transport, Host, Port, []}], #{ 52 | sync_start => true 53 | }) 54 | of 55 | {ok, _Conn} -> 56 | connect(Transport, Host, Port); 57 | {error, {already_started, _}} -> 58 | connect(Transport, Host, Port); 59 | {error, _Reason} = Error -> 60 | Error 61 | end; 62 | {ok, {_Conn, _Interceptor}} -> 63 | ok 64 | end. 65 | 66 | -spec batch_update( 67 | Fun :: fun((Action, T) -> ok), 68 | List :: [{Action, [T]}], 69 | BatchSleep :: non_neg_integer() 70 | ) -> 71 | ok | {error, any()} 72 | when 73 | Action :: add | remove. 74 | batch_update(Fun, List, BatchSleep) -> 75 | lists:foreach( 76 | fun({Action, Els}) -> 77 | lager:info( 78 | "batch update [action: ~p] [count: ~p] [batch_sleep: ~pms]", 79 | [Action, erlang:length(Els), BatchSleep] 80 | ), 81 | lists:foldl( 82 | fun(El, Idx) -> 83 | %% we pause between every batch of 1k to not oversaturate 84 | %% our connection to the config service. 85 | case Idx rem 1000 of 86 | 0 -> timer:sleep(BatchSleep); 87 | _ -> ok 88 | end, 89 | ok = Fun(Action, El), 90 | Idx + 1 91 | end, 92 | 1, 93 | Els 94 | ) 95 | end, 96 | List 97 | ). 98 | 99 | %% ------------------------------------------------------------------ 100 | %% EUNIT Tests 101 | %% ------------------------------------------------------------------ 102 | -ifdef(TEST). 103 | -include_lib("eunit/include/eunit.hrl"). 104 | 105 | batch_update_test() -> 106 | Ok = fun(_, _) -> ok end, 107 | 108 | %% warmup 109 | {_, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 500)}], timer:seconds(0)]), 110 | {_, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 500)}], timer:seconds(5)]), 111 | 112 | {Time0, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 2500)}], 0]), 113 | {Time1, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 2500)}], 50]), 114 | {Time2, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 2500)}], 100]), 115 | {Time3, _} = timer:tc(?MODULE, batch_update, [Ok, [{add, lists:seq(1, 2500)}], 150]), 116 | %% ct:print("~p < ~p < ~p < ~p", [ 117 | %% erlang:convert_time_unit(Time0, microsecond, millisecond), 118 | %% erlang:convert_time_unit(Time1, microsecond, millisecond), 119 | %% erlang:convert_time_unit(Time2, microsecond, millisecond), 120 | %% erlang:convert_time_unit(Time3, microsecond, millisecond) 121 | %% ]), 122 | ?assert(Time0 < Time1), 123 | ?assert(Time1 < Time2), 124 | ?assert(Time2 < Time3), 125 | 126 | ok. 127 | 128 | -endif. 129 | -------------------------------------------------------------------------------- /src/grpc/router_skf_reconcile.erl: -------------------------------------------------------------------------------- 1 | -module(router_skf_reconcile). 2 | 3 | -export([ 4 | new/1, 5 | %% Getters 6 | update_chunks/1, 7 | local/1, 8 | %% Counts 9 | update_chunks_count/1, 10 | remote_count/1, 11 | local_count/1, 12 | updates_count/1, 13 | add_count/1, 14 | remove_count/1 15 | ]). 16 | 17 | -record(reconcile, { 18 | remote :: router_ics_skf_worker:skfs(), 19 | remote_count :: non_neg_integer(), 20 | %% 21 | local :: router_ics_skf_worker:skfs(), 22 | local_count :: non_neg_integer(), 23 | %% 24 | updates :: router_ics_skf_worker:skf_updates(), 25 | updates_count :: non_neg_integer(), 26 | update_chunks :: list(router_ics_skf_worker:skf_updates()), 27 | update_chunks_count :: non_neg_integer(), 28 | %% 29 | add_count :: non_neg_integer(), 30 | remove_count :: non_neg_integer() 31 | }). 32 | 33 | -type reconcile() :: #reconcile{}. 34 | 35 | %% ------------------------------------------------------------------ 36 | %% Exports 37 | %% ------------------------------------------------------------------ 38 | 39 | -spec new(#{ 40 | remote := router_ics_skf_worker:skfs(), 41 | local := router_ics_skf_worker:skfs(), 42 | chunk_size := non_neg_integer() 43 | }) -> 44 | reconcile(). 45 | new(#{remote := Remote, local := Local, chunk_size := ChunkSize}) -> 46 | Diff = router_ics_skf_worker:diff_skf_to_updates(#{remote => Remote, local => Local}), 47 | DiffChunks = chunk(ChunkSize, Diff), 48 | 49 | #{ 50 | to_add := ToAdd, 51 | to_remove := ToRemove 52 | } = router_ics_skf_worker:partition_updates_by_action(Diff), 53 | 54 | #reconcile{ 55 | remote = Remote, 56 | remote_count = erlang:length(Remote), 57 | 58 | local = Local, 59 | local_count = erlang:length(Local), 60 | 61 | updates = Diff, 62 | updates_count = erlang:length(Diff), 63 | update_chunks = DiffChunks, 64 | update_chunks_count = erlang:length(DiffChunks), 65 | 66 | add_count = erlang:length(ToAdd), 67 | remove_count = erlang:length(ToRemove) 68 | }. 69 | 70 | -spec update_chunks(reconcile()) -> list(router_ics_skf_worker:skf_updates()). 71 | update_chunks(#reconcile{update_chunks = UpdateChunks}) -> 72 | UpdateChunks. 73 | 74 | -spec local(reconcile()) -> router_ics_skf_worker:skfs(). 75 | local(#reconcile{local = Local}) -> 76 | Local. 77 | 78 | -spec update_chunks_count(reconcile()) -> non_neg_integer(). 79 | update_chunks_count(#reconcile{update_chunks_count = UpdateChunksCount}) -> 80 | UpdateChunksCount. 81 | 82 | -spec remote_count(reconcile()) -> non_neg_integer(). 83 | remote_count(#reconcile{remote_count = RemoteCount}) -> 84 | RemoteCount. 85 | 86 | -spec local_count(reconcile()) -> non_neg_integer(). 87 | local_count(#reconcile{local_count = LocalCount}) -> 88 | LocalCount. 89 | 90 | -spec updates_count(reconcile()) -> non_neg_integer(). 91 | updates_count(#reconcile{updates_count = UpdatesCount}) -> 92 | UpdatesCount. 93 | 94 | -spec add_count(reconcile()) -> non_neg_integer(). 95 | add_count(#reconcile{add_count = AddCount}) -> 96 | AddCount. 97 | 98 | -spec remove_count(reconcile()) -> non_neg_integer(). 99 | remove_count(#reconcile{remove_count = RemoveCount}) -> 100 | RemoveCount. 101 | 102 | %% ------------------------------------------------------------------ 103 | %% Internal 104 | %% ------------------------------------------------------------------ 105 | 106 | chunk(Limit, Els) -> 107 | chunk(Limit, Els, []). 108 | 109 | chunk(_, [], Acc) -> 110 | lists:reverse(Acc); 111 | chunk(Limit, Els, Acc) -> 112 | case erlang:length(Els) > Limit of 113 | true -> 114 | {Chunk, Rest} = lists:split(Limit, Els), 115 | chunk(Limit, Rest, [Chunk | Acc]); 116 | false -> 117 | chunk(Limit, [], [Els | Acc]) 118 | end. 119 | -------------------------------------------------------------------------------- /src/metrics/router_metrics_reporter.erl: -------------------------------------------------------------------------------- 1 | -module(router_metrics_reporter). 2 | 3 | -behaviour(elli_handler). 4 | 5 | -include_lib("elli/include/elli.hrl"). 6 | 7 | -export([ 8 | handle/2, 9 | handle_event/3 10 | ]). 11 | 12 | handle(Req, _Args) -> 13 | handle(Req#req.method, elli_request:path(Req), Req). 14 | 15 | %% Expose /metrics for Prometheus to pull 16 | handle('GET', [<<"metrics">>], _Req) -> 17 | {ok, [], prometheus_text_format:format()}; 18 | %% Expose /devaddr to export a list of devices with there location and devaddr (use: https://kepler.gl/demo) 19 | handle('GET', [<<"devaddr">>, <<"json">>], _Req) -> 20 | case export_devaddr() of 21 | {ok, Devices} -> 22 | {ok, [], jsx:encode(Devices)}; 23 | {error, Reason} -> 24 | {500, [], Reason} 25 | end; 26 | handle('GET', [<<"devaddr">>, <<"csv">>], _Req) -> 27 | case export_devaddr() of 28 | {ok, Devices} -> 29 | {ok, [], csv_format(Devices)}; 30 | {error, Reason} -> 31 | {500, [], Reason} 32 | end; 33 | handle(_Verb, _Path, _Req) -> 34 | ignore. 35 | 36 | handle_event(_Event, _Data, _Args) -> 37 | ok. 38 | 39 | %% ------------------------------------------------------------------ 40 | %% Internal Function Definitions 41 | %% ------------------------------------------------------------------ 42 | 43 | -spec csv_format(list(map())) -> list(). 44 | csv_format(Devices) -> 45 | Header = "name,desc,latitude,longitude,color", 46 | CSV = lists:reverse( 47 | lists:foldl( 48 | fun(Map, Acc) -> 49 | case maps:is_key(devaddr, Map) of 50 | true -> 51 | Name = to_list(maps:get(devaddr, Map)) ++ ",", 52 | Desc = 53 | "Device name: " ++ 54 | to_list(maps:get(name, Map)) ++ 55 | " / Device ID: " ++ 56 | to_list(maps:get(id, Map)) ++ 57 | " / Hostspot ID: " ++ 58 | to_list(maps:get(hotspot_id, Map)) ++ 59 | " / Hostspot Name: " ++ to_list(maps:get(hotspot_name, Map)) ++ ",", 60 | Lat = io_lib:format("~.20f", [maps:get(lat, Map)]) ++ ",", 61 | Long = io_lib:format("~.20f", [maps:get(long, Map)]) ++ ",", 62 | Color = maps:get(color, Map, "green"), 63 | [Name ++ Desc ++ Lat ++ Long ++ Color | Acc]; 64 | false -> 65 | Name = to_list(maps:get(hotspot_name, Map)) ++ ",", 66 | Desc = "Hostspot ID: " ++ to_list(maps:get(hotspot_id, Map)) ++ ",", 67 | Lat = io_lib:format("~.20f", [maps:get(lat, Map)]) ++ ",", 68 | Long = io_lib:format("~.20f", [maps:get(long, Map)]) ++ ",", 69 | Color = maps:get(color, Map, "blue"), 70 | [Name ++ Desc ++ Lat ++ Long ++ Color | Acc] 71 | end 72 | end, 73 | [], 74 | Devices 75 | ) 76 | ), 77 | LineSep = io_lib:nl(), 78 | [Header, LineSep, string:join(CSV, LineSep), LineSep]. 79 | 80 | -spec export_devaddr() -> {ok, list(map())} | {error, binary()}. 81 | export_devaddr() -> 82 | case router_blockchain:privileged_maybe_get_blockchain() of 83 | undefined -> 84 | {error, <<"undefined_blockchain">>}; 85 | _Chain -> 86 | %% We don't use the chain here, we want to know it's in persistent_term 87 | %% for downstream calls that do block. 88 | Devices = lists:map( 89 | fun(Device) -> 90 | {HotspotID, HotspotName, Lat, Long} = get_location_info(Device), 91 | #{ 92 | device_id => router_device:id(Device), 93 | device_name => router_device:name(Device), 94 | device_devaddr => lorawan_utils:binary_to_hex( 95 | router_device:devaddr(Device) 96 | ), 97 | hotspot_id => erlang:list_to_binary(HotspotID), 98 | hotspot_name => erlang:list_to_binary(HotspotName), 99 | hotspot_lat => Lat, 100 | hotspot_long => Long 101 | } 102 | end, 103 | router_device_cache:get() 104 | ), 105 | {ok, Devices} 106 | end. 107 | 108 | -spec get_location_info(router_device:device()) -> {list(), list(), float(), float()}. 109 | get_location_info(Device) -> 110 | case router_device:location(Device) of 111 | undefined -> 112 | {"unasserted", "unasserted", 0.0, 0.0}; 113 | PubKeyBin -> 114 | B58 = libp2p_crypto:bin_to_b58(PubKeyBin), 115 | HotspotName = blockchain_utils:addr2name(PubKeyBin), 116 | case router_blockchain:get_hotspot_lat_lon(PubKeyBin) of 117 | {unknown, unknown} -> 118 | {B58, HotspotName, 0.0, 0.0}; 119 | {Lat, Long} -> 120 | {B58, HotspotName, Lat, Long} 121 | end 122 | end. 123 | 124 | -spec to_list(atom() | binary()) -> list(). 125 | to_list(A) when is_atom(A) -> 126 | erlang:atom_to_list(A); 127 | to_list(B) when is_binary(B) -> 128 | erlang:binary_to_list(B). 129 | -------------------------------------------------------------------------------- /src/router.app.src: -------------------------------------------------------------------------------- 1 | {application, router, [ 2 | {description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, {router_app, []}}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | syntax_tools, 10 | compiler, 11 | crypto, 12 | ssl, 13 | public_key, 14 | lager, 15 | ranch, 16 | libp2p, 17 | helium_proto, 18 | observer_cli, 19 | hackney, 20 | kvc, 21 | jsx, 22 | emqtt, 23 | erl_angry_purple_tiger, 24 | throttle, 25 | httpc_aws, 26 | websocket_client, 27 | erlang_v8, 28 | prometheus, 29 | elli, 30 | bbmustache, 31 | iso8601, 32 | clique, 33 | inet_cidr, 34 | erlang_lorawan, 35 | router_utils, 36 | throttle, 37 | e2qc 38 | ]}, 39 | {included_applications, [blockchain, grpcbox]}, 40 | {env, []}, 41 | {modules, []}, 42 | {maintainers, []}, 43 | {licenses, ["Apache 2.0"]}, 44 | {links, []} 45 | ]}. 46 | -------------------------------------------------------------------------------- /src/router_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc router public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(router_app). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, stop/1]). 12 | 13 | %%==================================================================== 14 | %% API 15 | %%==================================================================== 16 | 17 | start(_StartType, _StartArgs) -> 18 | case router_sup:start_link() of 19 | {error, _} = Error -> 20 | Error; 21 | Ok -> 22 | router_cli_registry:register_cli(), 23 | Ok 24 | end. 25 | 26 | %%-------------------------------------------------------------------- 27 | stop(_State) -> 28 | ok. 29 | 30 | %%==================================================================== 31 | %% Internal functions 32 | %%==================================================================== 33 | -------------------------------------------------------------------------------- /src/router_db.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router DB == 4 | %% @end 5 | %%%------------------------------------------------------------------- 6 | -module(router_db). 7 | 8 | -behavior(gen_server). 9 | 10 | %% ------------------------------------------------------------------ 11 | %% API Function Exports 12 | %% ------------------------------------------------------------------ 13 | -export([ 14 | start_link/1, 15 | get/0, 16 | get_xor_filter_devices/0, 17 | get_devices/0 18 | ]). 19 | 20 | %% ------------------------------------------------------------------ 21 | %% gen_server Function Exports 22 | %% ------------------------------------------------------------------ 23 | -export([ 24 | init/1, 25 | handle_call/3, 26 | handle_cast/2, 27 | handle_info/2, 28 | terminate/2, 29 | code_change/3 30 | ]). 31 | 32 | -define(SERVER, ?MODULE). 33 | -define(DB_FILE, "router.db"). 34 | -define(CFS, ["default", "devices", "xor_filter_devices"]). 35 | 36 | -record(state, { 37 | db :: rocksdb:db_handle(), 38 | cfs :: #{atom() => rocksdb:cf_handle()} 39 | }). 40 | 41 | %% ------------------------------------------------------------------ 42 | %% API Function Definitions 43 | %% ------------------------------------------------------------------ 44 | start_link(Args) -> 45 | gen_server:start_link({local, ?SERVER}, ?SERVER, Args, []). 46 | 47 | -spec get() -> {ok, rocksdb:db_handle(), [rocksdb:cf_handle()]}. 48 | get() -> 49 | gen_server:call(?SERVER, get). 50 | 51 | -spec get_xor_filter_devices() -> {ok, rocksdb:db_handle(), rocksdb:cf_handle()}. 52 | get_xor_filter_devices() -> 53 | gen_server:call(?SERVER, get_xor_filter_devices). 54 | 55 | -spec get_devices() -> {ok, rocksdb:db_handle(), rocksdb:cf_handle()}. 56 | get_devices() -> 57 | gen_server:call(?SERVER, get_devices). 58 | 59 | %% ------------------------------------------------------------------ 60 | %% gen_server Function Definitions 61 | %% ------------------------------------------------------------------ 62 | init([Dir] = Args) -> 63 | lager:info("~p init with ~p", [?SERVER, Args]), 64 | {ok, DB, CFs} = open_db(Dir), 65 | {ok, #state{db = DB, cfs = CFs}}. 66 | 67 | handle_call(get, _From, #state{db = DB, cfs = CFs} = State) -> 68 | #{default := DefaultCF, devices := DevicesCF} = CFs, 69 | {reply, {ok, DB, [DefaultCF, DevicesCF]}, State}; 70 | handle_call(get_xor_filter_devices, _From, #state{db = DB, cfs = CFs} = State) -> 71 | CF = maps:get(xor_filter_devices, CFs), 72 | {reply, {ok, DB, CF}, State}; 73 | handle_call(get_devices, _From, #state{db = DB, cfs = CFs} = State) -> 74 | CF = maps:get(devices, CFs), 75 | {reply, {ok, DB, CF}, State}; 76 | handle_call(_Msg, _From, State) -> 77 | lager:warning("rcvd unknown call msg: ~p from: ~p", [_Msg, _From]), 78 | {reply, ok, State}. 79 | 80 | handle_cast(_Msg, State) -> 81 | lager:warning("rcvd unknown cast msg: ~p", [_Msg]), 82 | {noreply, State}. 83 | 84 | handle_info(_Msg, State) -> 85 | lager:warning("rcvd unknown info msg: ~p", [_Msg]), 86 | {noreply, State}. 87 | 88 | code_change(_OldVsn, State, _Extra) -> 89 | {ok, State}. 90 | 91 | terminate(_Reason, #state{db = DB}) -> 92 | catch rocksdb:close(DB), 93 | ok. 94 | 95 | %% ------------------------------------------------------------------ 96 | %% Internal Function Definitions 97 | %% ------------------------------------------------------------------ 98 | 99 | -spec open_db(file:filename_all()) -> 100 | {ok, rocksdb:db_handle(), #{atom() => rocksdb:cf_handle()}} | {error, any()}. 101 | open_db(Dir) -> 102 | DBDir = filename:join(Dir, ?DB_FILE), 103 | ok = filelib:ensure_dir(DBDir), 104 | 105 | GlobalOpts = application:get_env(rocksdb, global_opts, []), 106 | 107 | DBOptions = [{create_if_missing, true}, {atomic_flush, true}] ++ GlobalOpts, 108 | 109 | CFOpts = GlobalOpts, 110 | 111 | DefaultCFs = ?CFS, 112 | ExistingCFs = 113 | case rocksdb:list_column_families(DBDir, DBOptions) of 114 | {ok, CFs0} -> 115 | CFs0; 116 | {error, _} -> 117 | ["default"] 118 | end, 119 | 120 | {ok, DB, OpenedCFs} = 121 | rocksdb:open_with_cf(DBDir, DBOptions, [{CF, CFOpts} || CF <- ExistingCFs]), 122 | 123 | L1 = lists:zip(ExistingCFs, OpenedCFs), 124 | L2 = lists:map( 125 | fun(CF) -> 126 | {ok, CF1} = rocksdb:create_column_family(DB, CF, CFOpts), 127 | {CF, CF1} 128 | end, 129 | DefaultCFs -- ExistingCFs 130 | ), 131 | L3 = L1 ++ L2, 132 | 133 | CFs = maps:from_list([ 134 | {erlang:list_to_atom(X), proplists:get_value(X, L3)} 135 | || X <- DefaultCFs 136 | ]), 137 | 138 | {ok, DB, CFs}. 139 | -------------------------------------------------------------------------------- /src/router_discovery_handler.erl: -------------------------------------------------------------------------------- 1 | -module(router_discovery_handler). 2 | 3 | -behavior(libp2p_framed_stream). 4 | 5 | %% ------------------------------------------------------------------ 6 | %% API Function Exports 7 | %% ------------------------------------------------------------------ 8 | 9 | -export([ 10 | server/4, 11 | client/2, 12 | version/0, 13 | dial/1, 14 | send/2 15 | ]). 16 | 17 | %% ------------------------------------------------------------------ 18 | %% libp2p_framed_stream Function Exports 19 | %% ------------------------------------------------------------------ 20 | -export([ 21 | init/3, 22 | handle_data/3, 23 | handle_info/3 24 | ]). 25 | 26 | -define(VERSION, "discovery/1.0.0"). 27 | 28 | -record(state, {}). 29 | 30 | %% ------------------------------------------------------------------ 31 | %% API Function Definitions 32 | %% ------------------------------------------------------------------ 33 | server(Connection, Path, _TID, Args) -> 34 | libp2p_framed_stream:server(?MODULE, Connection, [Path | Args]). 35 | 36 | client(Connection, Args) -> 37 | libp2p_framed_stream:client(?MODULE, Connection, Args). 38 | 39 | -spec version() -> string(). 40 | version() -> 41 | ?VERSION. 42 | 43 | -spec dial(PubKeyBin :: libp2p_crypto:pubkey_bin()) -> {ok, pid()} | {error, term()}. 44 | dial(PubKeyBin) -> 45 | libp2p_swarm:dial_framed_stream( 46 | blockchain_swarm:swarm(), 47 | libp2p_crypto:pubkey_bin_to_p2p(PubKeyBin), 48 | ?VERSION, 49 | ?MODULE, 50 | [] 51 | ). 52 | 53 | -spec send(Pid :: pid(), Data :: binary()) -> ok. 54 | send(Pid, Data) -> 55 | Pid ! {send, Data}, 56 | ok. 57 | 58 | %% ------------------------------------------------------------------ 59 | %% libp2p_framed_stream Function Definitions 60 | %% ------------------------------------------------------------------ 61 | 62 | init(client, _Conn, _Args) -> 63 | lager:info("client started with ~p", [_Args]), 64 | {ok, #state{}}; 65 | init(server, _Conn, _Args) -> 66 | lager:info("server started with ~p", [_Args]), 67 | {ok, #state{}}. 68 | 69 | handle_data(_Type, _Data, State) -> 70 | lager:warning("~p got data ~p", [_Type, _Data]), 71 | {noreply, State}. 72 | 73 | handle_info(client, {send, Data}, State) -> 74 | {noreply, State, Data}; 75 | handle_info(_Type, _Msg, State) -> 76 | lager:warning("test ~p got info ~p", [_Type, _Msg]), 77 | {noreply, State}. 78 | 79 | %% ------------------------------------------------------------------ 80 | %% Internal Function Definitions 81 | %% ------------------------------------------------------------------ 82 | -------------------------------------------------------------------------------- /src/router_handler.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc 3 | %% == Router Handler == 4 | %% Routes a packet depending on Helium Console provided information. 5 | %% @end 6 | %%%------------------------------------------------------------------- 7 | -module(router_handler). 8 | 9 | -behavior(libp2p_framed_stream). 10 | 11 | -include_lib("helium_proto/include/blockchain_state_channel_v1_pb.hrl"). 12 | 13 | -include("router_device_worker.hrl"). 14 | 15 | %% ------------------------------------------------------------------ 16 | %% API Function Exports 17 | %% ------------------------------------------------------------------ 18 | 19 | -export([ 20 | server/4, 21 | client/2, 22 | add_stream_handler/1, 23 | version/0 24 | ]). 25 | 26 | %% ------------------------------------------------------------------ 27 | %% libp2p_framed_stream Function Exports 28 | %% ------------------------------------------------------------------ 29 | -export([ 30 | init/3, 31 | handle_data/3, 32 | handle_info/3 33 | ]). 34 | 35 | -define(VERSION, "simple_http/1.0.0"). 36 | 37 | -record(state, {}). 38 | 39 | %% ------------------------------------------------------------------ 40 | %% API Function Definitions 41 | %% ------------------------------------------------------------------ 42 | server(Connection, Path, _TID, Args) -> 43 | libp2p_framed_stream:server(?MODULE, Connection, [Path | Args]). 44 | 45 | client(Connection, Args) -> 46 | libp2p_framed_stream:client(?MODULE, Connection, Args). 47 | 48 | -spec add_stream_handler(pid()) -> ok. 49 | add_stream_handler(Swarm) -> 50 | ok = libp2p_swarm:add_stream_handler( 51 | Swarm, 52 | ?VERSION, 53 | {libp2p_framed_stream, server, [?MODULE, self()]} 54 | ). 55 | 56 | -spec version() -> string(). 57 | version() -> 58 | ?VERSION. 59 | 60 | %% ------------------------------------------------------------------ 61 | %% libp2p_framed_stream Function Definitions 62 | %% ------------------------------------------------------------------ 63 | init(server, _Conn, _Args) -> 64 | {ok, #state{}}; 65 | init(client, _Conn, _Args) -> 66 | {ok, #state{}}. 67 | 68 | handle_data(server, Data, State) -> 69 | lager:debug("got data ~p", [Data]), 70 | case decode_data(Data) of 71 | {error, _Reason} -> 72 | lager:debug("failed to transmit data ~p", [_Reason]); 73 | {ok, #packet_pb{type = lorawan} = Packet0, PubKeyBin, Region} -> 74 | router_device_routing:handle_packet( 75 | Packet0, 76 | erlang:system_time(millisecond), 77 | PubKeyBin, 78 | Region 79 | ); 80 | {ok, #packet_pb{type = _Type} = _Packet, PubKeyBin, _Region} -> 81 | {ok, _AName} = erl_angry_purple_tiger:animal_name(libp2p_crypto:bin_to_b58(PubKeyBin)), 82 | lager:error("unknown packet type ~p coming from ~p: ~p", [_Type, _AName, _Packet]) 83 | end, 84 | {noreply, State}; 85 | handle_data(_Type, _Bin, State) -> 86 | lager:warning("~p got data ~p", [_Type, _Bin]), 87 | {noreply, State}. 88 | 89 | handle_info(server, {send_response, Resp}, State) -> 90 | Data = blockchain_state_channel_message_v1:encode(Resp), 91 | {noreply, State, Data}; 92 | handle_info(server, {packet, undefined}, State) -> 93 | {noreply, State}; 94 | handle_info(server, {packet, #packet_pb{} = Packet}, State) -> 95 | {noreply, State, encode_resp(Packet)}; 96 | handle_info(_Type, _Msg, State) -> 97 | lager:warning("~p got info ~p", [_Type, _Msg]), 98 | {noreply, State}. 99 | 100 | %% ------------------------------------------------------------------ 101 | %% Internal Function Definitions 102 | %% ------------------------------------------------------------------ 103 | 104 | -spec encode_resp(#packet_pb{}) -> binary(). 105 | encode_resp(Packet) -> 106 | Resp = #blockchain_state_channel_response_v1_pb{accepted = true, downlink = Packet}, 107 | Msg = #blockchain_state_channel_message_v1_pb{msg = {response, Resp}}, 108 | blockchain_state_channel_v1_pb:encode_msg(Msg). 109 | 110 | -spec decode_data(binary()) -> 111 | {ok, #packet_pb{}, libp2p_crypto:pubkey_bin(), atom()} | {error, any()}. 112 | decode_data(Data) -> 113 | try blockchain_state_channel_v1_pb:decode_msg(Data, blockchain_state_channel_message_v1_pb) of 114 | #blockchain_state_channel_message_v1_pb{msg = {packet, Packet}} -> 115 | #blockchain_state_channel_packet_v1_pb{ 116 | packet = HeliumPacket, 117 | hotspot = PubKeyBin, 118 | region = Region 119 | } = Packet, 120 | {ok, HeliumPacket, PubKeyBin, Region}; 121 | _ -> 122 | {error, unhandled_message} 123 | catch 124 | _E:_R -> 125 | {error, {decode_failed, _E, _R}} 126 | end. 127 | -------------------------------------------------------------------------------- /src/router_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc router top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(router_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SUP(I, Args), #{ 17 | id => I, 18 | start => {I, start_link, Args}, 19 | restart => permanent, 20 | shutdown => 5000, 21 | type => supervisor, 22 | modules => [I] 23 | }). 24 | 25 | -define(WORKER(I, Args), #{ 26 | id => I, 27 | start => {I, start_link, Args}, 28 | restart => permanent, 29 | shutdown => 5000, 30 | type => worker, 31 | modules => [I] 32 | }). 33 | 34 | -define(WORKER(I, Mod, Args), #{ 35 | id => I, 36 | start => {Mod, start_link, Args}, 37 | restart => permanent, 38 | shutdown => 5000, 39 | type => worker, 40 | modules => [I] 41 | }). 42 | 43 | -define(FLAGS, #{ 44 | strategy => rest_for_one, 45 | intensity => 1, 46 | period => 5 47 | }). 48 | 49 | -define(SERVER, ?MODULE). 50 | 51 | %%==================================================================== 52 | %% API functions 53 | %%==================================================================== 54 | 55 | start_link() -> 56 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 57 | 58 | %%==================================================================== 59 | %% Supervisor callbacks 60 | %%==================================================================== 61 | 62 | %% Child :: #{id => Id, start => {M, F, A}} 63 | %% Optional keys are restart, shutdown, type, modules. 64 | %% Before OTP 18 tuples must be used to specify a child. e.g. 65 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 66 | init([]) -> 67 | BaseDir = application:get_env(blockchain, base_dir, "data"), 68 | ok = router_decoder:init_ets(), 69 | ok = router_console_dc_tracker:init_ets(), 70 | ok = router_console_api:init_ets(), 71 | ok = router_device_stats:init(), 72 | ok = ru_denylist:init(BaseDir), 73 | ok = libp2p_crypto:set_network(application:get_env(blockchain, network, mainnet)), 74 | ok = router_ics_gateway_location_worker:init_ets(), 75 | 76 | {ok, _} = application:ensure_all_started(ranch), 77 | {ok, _} = application:ensure_all_started(lager), 78 | %% This is needed by grpcbox 79 | {ok, _} = application:ensure_all_started(gproc), 80 | 81 | SeedNodes = 82 | case application:get_env(blockchain, seed_nodes) of 83 | {ok, ""} -> []; 84 | {ok, Seeds} -> string:split(Seeds, ",", all); 85 | _ -> [] 86 | end, 87 | 88 | SwarmKey = router_utils:get_swarm_key_location(), 89 | ok = filelib:ensure_dir(SwarmKey), 90 | Key = 91 | case libp2p_crypto:load_keys(SwarmKey) of 92 | {ok, #{secret := PrivKey, public := PubKey}} -> 93 | {PubKey, libp2p_crypto:mk_sig_fun(PrivKey), libp2p_crypto:mk_ecdh_fun(PrivKey)}; 94 | {error, enoent} -> 95 | KeyMap = 96 | #{secret := PrivKey, public := PubKey} = libp2p_crypto:generate_keys( 97 | ecc_compact 98 | ), 99 | ok = libp2p_crypto:save_keys(KeyMap, SwarmKey), 100 | {PubKey, libp2p_crypto:mk_sig_fun(PrivKey), libp2p_crypto:mk_ecdh_fun(PrivKey)} 101 | end, 102 | ok = router_blockchain:save_key(Key), 103 | 104 | BlockchainOpts = [ 105 | {key, Key}, 106 | {seed_nodes, SeedNodes}, 107 | {max_inbound_connections, 10}, 108 | {port, application:get_env(blockchain, port, 0)}, 109 | {base_dir, BaseDir}, 110 | {update_dir, application:get_env(blockchain, update_dir, undefined)} 111 | ], 112 | SCWorkerOpts = #{}, 113 | DBOpts = [BaseDir], 114 | MetricsOpts = #{}, 115 | POCDenyListArgs = 116 | case 117 | { 118 | application:get_env(router, denylist_keys, undefined), 119 | application:get_env(router, denylist_url, undefined) 120 | } 121 | of 122 | {undefined, _} -> 123 | #{}; 124 | {_, undefined} -> 125 | #{}; 126 | {DenyListKeys, DenyListUrl} -> 127 | #{ 128 | denylist_keys => DenyListKeys, 129 | denylist_url => DenyListUrl, 130 | denylist_base_dir => BaseDir, 131 | denylist_check_timer => {immediate, timer:hours(12)} 132 | } 133 | end, 134 | 135 | {PubKey0, SigFun, _} = Key, 136 | PubKeyBin = libp2p_crypto:pubkey_to_bin(PubKey0), 137 | ICSOptsDefault = application:get_env(router, ics, #{}), 138 | ICSOpts = ICSOptsDefault#{pubkey_bin => PubKeyBin, sig_fun => SigFun}, 139 | 140 | {ok, HLC} = 141 | cream:new( 142 | 200_000, 143 | [ 144 | {initial_capacity, 20_000}, 145 | {seconds_to_live, 600} 146 | ] 147 | ), 148 | ok = persistent_term:put(hotspot_location_cache, HLC), 149 | {ok, DevaddrCache} = cream:new(1, [{seconds_to_live, 10}]), 150 | ok = persistent_term:put(devaddr_subnets_cache, DevaddrCache), 151 | 152 | ChainWorkers = 153 | case router_blockchain:is_chain_dead() of 154 | true -> 155 | []; 156 | false -> 157 | [ 158 | ?SUP(blockchain_sup, [BlockchainOpts]), 159 | ?WORKER(router_sc_worker, [SCWorkerOpts]), 160 | ?WORKER(router_xor_filter_worker, [#{}]) 161 | ] 162 | end, 163 | 164 | {ok, 165 | {?FLAGS, 166 | [ 167 | %% Deprecated 168 | ?WORKER(ru_poc_denylist, [POCDenyListArgs]), 169 | ?WORKER(router_metrics, [MetricsOpts]), 170 | ?WORKER(router_db, [DBOpts]) 171 | ] ++ ChainWorkers ++ 172 | [ 173 | ?SUP(router_devices_sup, []), 174 | ?SUP(router_decoder_sup, []), 175 | ?SUP(router_console_sup, []), 176 | 177 | ?SUP(grpcbox_sup, []), 178 | ?WORKER(router_grpc_client_worker, []), 179 | 180 | %% Anything under here needs CS to work 181 | ?WORKER(router_device_devaddr, [ICSOpts]), 182 | ?WORKER(router_ics_eui_worker, [ICSOpts]), 183 | ?WORKER(router_ics_skf_worker, [ICSOpts]), 184 | 185 | %% This will open the flood gates 186 | ?WORKER(router_grpc_server_worker, []) 187 | ]}}. 188 | 189 | %%==================================================================== 190 | %% Internal functions 191 | %%==================================================================== 192 | -------------------------------------------------------------------------------- /test/router_ct_macros.hrl: -------------------------------------------------------------------------------- 1 | %-define(DEVEUI, rand:uniform(trunc(math:pow(2, 64)))). 2 | %-define(APPEUI, rand:uniform(trunc(math:pow(2, 64)))). 3 | -define(APPEUI, <<0, 0, 0, 2, 0, 0, 0, 1>>). 4 | -define(DEVEUI, <<0, 0, 0, 0, 0, 0, 0, 1>>). 5 | -define(APPKEY, 6 | <<16#2B, 16#7E, 16#15, 16#16, 16#28, 16#AE, 16#D2, 16#A6, 16#AB, 16#F7, 16#15, 16#88, 16#09, 7 | 16#CF, 16#4F, 16#3C>> 8 | ). 9 | 10 | -define(JOIN_REQUEST, 2#000). 11 | -------------------------------------------------------------------------------- /test/router_ct_utils.erl: -------------------------------------------------------------------------------- 1 | -module(router_ct_utils). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -include_lib("blockchain/include/blockchain_vars.hrl"). 6 | 7 | -include("console_test.hrl"). 8 | -include("router_ct_macros.hrl"). 9 | 10 | -export([ 11 | init_per_testcase/3, 12 | end_per_testcase/2 13 | ]). 14 | 15 | %% configures one router by default 16 | init_per_testcase(Mod, TestCase, Config0) -> 17 | init_per_testcase(Mod, TestCase, Config0, 1). 18 | 19 | init_per_testcase(Mod, TestCase, Config0, NumRouters) -> 20 | MinerConfig = miner_test:init_per_testcase(Mod, TestCase, Config0), 21 | RouterConfig = init_router_config(MinerConfig, NumRouters), 22 | 23 | MinerConfig ++ RouterConfig. 24 | 25 | init_router_config(Config, NumRouters) -> 26 | BaseDir = ?config(base_dir, Config), 27 | LogDir = ?config(log_dir, Config), 28 | MinerListenAddrs = ?config(miner_listen_addrs, Config), 29 | Miners = ?config(miners, Config), 30 | 31 | %% Router configuration 32 | TotalRouters = os:getenv("R", NumRouters), 33 | Port = list_to_integer(os:getenv("PORT", "0")), 34 | RoutersAndPorts = miner_test:init_ports(Config, TotalRouters), 35 | RouterKeys = miner_test:init_keys(Config, RoutersAndPorts), 36 | SeedNodes = [], 37 | 38 | %% NOTE: elli must be running before router nodes start 39 | Tab = ets:new(router_ct_utils, [public, set]), 40 | ElliOpts = [ 41 | {callback, console_callback}, 42 | {callback_args, #{ 43 | forward => self(), 44 | ets => Tab, 45 | app_key => ?APPKEY, 46 | app_eui => ?APPEUI, 47 | dev_eui => ?DEVEUI 48 | }}, 49 | {port, 3000} 50 | ], 51 | {ok, ElliPid} = elli:start_link(ElliOpts), 52 | 53 | %% Get router config results 54 | RouterConfigResult = router_config_result(LogDir, BaseDir, Port, SeedNodes, RouterKeys), 55 | 56 | %% Gather router nodes 57 | Routers = [M || {M, _} <- RoutersAndPorts], 58 | 59 | %% check that the config loaded correctly on each router 60 | true = miner_test:check_config_result(RouterConfigResult), 61 | 62 | %% Gather router listen addrs 63 | RouterListenAddrs = miner_test:acc_listen_addrs(Routers), 64 | 65 | %% connect nodes 66 | true = miner_test:connect_addrs(Miners, RouterListenAddrs), 67 | true = miner_test:connect_addrs(Routers, MinerListenAddrs), 68 | 69 | %% make sure routers are also talking to the miners 70 | true = miner_test:check_gossip(Routers, MinerListenAddrs), 71 | true = miner_test:check_gossip(Miners, RouterListenAddrs), 72 | 73 | %% accumulate the pubkey_bins of each miner 74 | RouterPubkeyBins = miner_test:acc_pubkey_bins(Routers), 75 | 76 | %% add both miners and router to cover 77 | {ok, _} = ct_cover:add_nodes(Miners ++ Routers), 78 | 79 | %% wait until we get confirmation the miners are fully up 80 | %% which we are determining by the miner_consensus_mgr being registered 81 | ok = miner_test:wait_for_registration(Miners, miner_consensus_mgr), 82 | 83 | [ 84 | {routers, Routers}, 85 | {router_keys, RouterKeys}, 86 | {router_pubkey_bins, RouterPubkeyBins}, 87 | {elli, ElliPid} 88 | | Config 89 | ]. 90 | 91 | end_per_testcase(TestCase, Config) -> 92 | Miners = ?config(miners, Config), 93 | Routers = ?config(routers, Config), 94 | miner_test:pmap(fun(Miner) -> ct_slave:stop(Miner) end, Miners), 95 | miner_test:pmap(fun(Router) -> ct_slave:stop(Router) end, Routers), 96 | case ?config(tc_status, Config) of 97 | ok -> 98 | %% test passed, we can cleanup 99 | miner_test:cleanup_per_testcase(TestCase, Config); 100 | _ -> 101 | %% leave results alone for analysis 102 | ok 103 | end, 104 | {comment, done}. 105 | 106 | router_config_result(LogDir, BaseDir, Port, SeedNodes, RouterKeys) -> 107 | miner_test:pmap( 108 | fun({Router, {_TCPPort1, _UDPPort1}, _ECDH, _PubKey, _Addr, _SigFun}) -> 109 | ct:pal("Router ~p", [Router]), 110 | ct_rpc:call(Router, cover, start, []), 111 | ct_rpc:call(Router, application, load, [lager]), 112 | ct_rpc:call(Router, application, load, [blockchain]), 113 | ct_rpc:call(Router, application, load, [libp2p]), 114 | ct_rpc:call(Router, application, load, [router]), 115 | %% give each node its own log directory 116 | LogRoot = LogDir ++ "_router_" ++ atom_to_list(Router), 117 | ct_rpc:call(Router, application, set_env, [lager, log_root, LogRoot]), 118 | ct_rpc:call(Router, lager, set_loglevel, [ 119 | {lager_file_backend, "log/console.log"}, 120 | debug 121 | ]), 122 | 123 | %% set blockchain configuration 124 | #{public := PubKey, secret := PrivKey} = libp2p_crypto:generate_keys(ecc_compact), 125 | Key = {PubKey, libp2p_crypto:mk_sig_fun(PrivKey), libp2p_crypto:mk_ecdh_fun(PrivKey)}, 126 | RouterBaseDir = BaseDir ++ "_router_" ++ atom_to_list(Router), 127 | ct_rpc:call(Router, application, set_env, [blockchain, base_dir, RouterBaseDir]), 128 | ct_rpc:call(Router, application, set_env, [blockchain, port, Port]), 129 | ct_rpc:call(Router, application, set_env, [blockchain, seed_nodes, SeedNodes]), 130 | ct_rpc:call(Router, application, set_env, [blockchain, key, Key]), 131 | ct_rpc:call(Router, application, set_env, [ 132 | blockchain, 133 | sc_client_handler, 134 | router_sc_client_handler 135 | ]), 136 | ct_rpc:call(Router, application, set_env, [ 137 | blockchain, 138 | sc_packet_handler, 139 | router_device_routing 140 | ]), 141 | 142 | %% Set router configuration 143 | ct_rpc:call(Router, application, set_env, [router, base_dir, RouterBaseDir]), 144 | ct_rpc:call(Router, application, set_env, [router, port, Port]), 145 | ct_rpc:call(Router, application, set_env, [router, seed_nodes, SeedNodes]), 146 | ct_rpc:call(Router, application, set_env, [router, oui, 1]), 147 | ct_rpc:call(Router, application, set_env, [ 148 | router, 149 | router_console_api, 150 | [ 151 | {endpoint, ?CONSOLE_URL}, 152 | {ws_endpoint, ?CONSOLE_WS_URL}, 153 | {secret, <<"yolo">>} 154 | ] 155 | ]), 156 | ct_rpc:call(Router, application, set_env, [ 157 | router, 158 | metrics, 159 | [{reporters, []}] 160 | ]), 161 | {ok, StartedApps} = ct_rpc:call(Router, application, ensure_all_started, [router]), 162 | ct:pal("Router: ~p, StartedApps: ~p", [Router, StartedApps]) 163 | end, 164 | RouterKeys 165 | ). 166 | -------------------------------------------------------------------------------- /test/router_decoder_custom_sup_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_decoder_custom_sup_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_testcase/2, 6 | end_per_testcase/2 7 | ]). 8 | 9 | -export([ 10 | v8_recovery_test/1 11 | ]). 12 | 13 | -include_lib("common_test/include/ct.hrl"). 14 | -include_lib("eunit/include/eunit.hrl"). 15 | 16 | %%-------------------------------------------------------------------- 17 | %% COMMON TEST CALLBACK FUNCTIONS 18 | %%-------------------------------------------------------------------- 19 | 20 | %%-------------------------------------------------------------------- 21 | %% @public 22 | %% @doc 23 | %% Running tests for this suite 24 | %% @end 25 | %%-------------------------------------------------------------------- 26 | all() -> 27 | [ 28 | v8_recovery_test 29 | ]. 30 | 31 | %%-------------------------------------------------------------------- 32 | %% TEST CASE SETUP 33 | %%-------------------------------------------------------------------- 34 | init_per_testcase(TestCase, Config) -> 35 | test_utils:init_per_testcase(TestCase, Config). 36 | 37 | %%-------------------------------------------------------------------- 38 | %% TEST CASE TEARDOWN 39 | %%-------------------------------------------------------------------- 40 | end_per_testcase(TestCase, Config) -> 41 | test_utils:end_per_testcase(TestCase, Config). 42 | 43 | %%-------------------------------------------------------------------- 44 | %% TEST CASES 45 | %%-------------------------------------------------------------------- 46 | 47 | v8_recovery_test(Config) -> 48 | %% Set console to decoder channel mode 49 | Tab = proplists:get_value(ets, Config), 50 | ets:insert(Tab, {channel_type, decoder}), 51 | 52 | test_utils:join_device(Config), 53 | 54 | %% Waiting for reply from router to hotspot 55 | test_utils:wait_state_channel_message(1250), 56 | 57 | ValidFunction = <<"function Decoder(a,b,c) { return 42; }">>, 58 | ValidDecoder = router_decoder:new( 59 | <<"valid">>, 60 | custom, 61 | #{function => ValidFunction} 62 | ), 63 | Result = 42, 64 | BogusFunction = <<"function Decoder(a,b,c) { crash @ here! }">>, 65 | BogusDecoder = router_decoder:new( 66 | <<"bogus">>, 67 | custom, 68 | #{function => BogusFunction} 69 | ), 70 | Payload = erlang:binary_to_list(base64:decode(<<"H4Av/xACRU4=">>)), 71 | Port = 6, 72 | Uplink = #{}, 73 | 74 | %% One context sends a valid function definiton and another sends 75 | %% bogus javascript, causing the V8 VM to be restarted. The first 76 | %% context should then recover. 77 | {ok, Pid1} = router_decoder_custom_sup:add(ValidDecoder), 78 | ?assertMatch( 79 | {ok, Result}, 80 | router_decoder_custom_worker:decode(Pid1, Payload, Port, Uplink) 81 | ), 82 | ?assertMatch( 83 | {ok, Result}, 84 | router_decoder_custom_worker:decode(Pid1, Payload, Port, Uplink) 85 | ), 86 | ?assertMatch( 87 | {ok, Result}, 88 | router_decoder_custom_worker:decode(Pid1, Payload, Port, Uplink) 89 | ), 90 | ?assert(erlang:is_process_alive(Pid1)), 91 | 92 | %% Even though JS was bad, we still get a Pid for less BEAM runtime overhead overall 93 | {ok, Pid2} = router_decoder_custom_sup:add(BogusDecoder), 94 | ?assertNotMatch(Pid1, Pid2), 95 | ?assertMatch( 96 | {error, ignoring_invalid_javascript}, 97 | router_decoder_custom_worker:decode(Pid2, Payload, Port, Uplink) 98 | ), 99 | ?assertMatch( 100 | {error, ignoring_invalid_javascript}, 101 | router_decoder_custom_worker:decode(Pid2, Payload, Port, Uplink) 102 | ), 103 | ?assertMatch( 104 | {error, ignoring_invalid_javascript}, 105 | router_decoder_custom_worker:decode(Pid2, Payload, Port, Uplink) 106 | ), 107 | 108 | ?assert(erlang:is_process_alive(Pid1)), 109 | ?assertMatch( 110 | {ok, Result}, 111 | router_decoder_custom_worker:decode(Pid1, Payload, Port, Uplink) 112 | ), 113 | ok. 114 | -------------------------------------------------------------------------------- /test/router_decoder_custom_worker_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_decoder_custom_worker_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_testcase/2, 6 | end_per_testcase/2 7 | ]). 8 | 9 | -export([ 10 | avoid_call_to_bad_js_test/1 11 | ]). 12 | 13 | -include_lib("common_test/include/ct.hrl"). 14 | -include_lib("eunit/include/eunit.hrl"). 15 | 16 | %%-------------------------------------------------------------------- 17 | %% @public 18 | %% @doc 19 | %% Running tests for this suite 20 | %% @end 21 | %%-------------------------------------------------------------------- 22 | all() -> 23 | [avoid_call_to_bad_js_test]. 24 | 25 | %%-------------------------------------------------------------------- 26 | %% TEST CASE SETUP 27 | %%-------------------------------------------------------------------- 28 | init_per_testcase(_TestCase, Config) -> 29 | Config. 30 | 31 | %%-------------------------------------------------------------------- 32 | %% TEST CASE TEARDOWN 33 | %%-------------------------------------------------------------------- 34 | end_per_testcase(_TestCase, Config) -> 35 | Config. 36 | 37 | %%-------------------------------------------------------------------- 38 | %% TEST CASES 39 | %%-------------------------------------------------------------------- 40 | 41 | avoid_call_to_bad_js_test(_Config) -> 42 | InvalidDecoder = <<"function Decoder(a,b,c) { crash + burn; }">>, 43 | {ok, VM} = router_v8:start_link(#{}), 44 | V8VM = 45 | case erlang_v8:start_vm() of 46 | {ok, V8Pid} -> V8Pid; 47 | {error, {already_started, V8Pid}} -> V8Pid 48 | end, 49 | Args = #{ 50 | id => <<"decoder_id">>, 51 | hash => <<"cafef00d">>, 52 | function => InvalidDecoder, 53 | vm => V8VM 54 | }, 55 | {ok, Pid} = router_decoder_custom_worker:start_link(Args), 56 | 57 | Payload = <<"doesn't matter for this test case">>, 58 | Port = 6, 59 | Uplink = #{}, 60 | %% First time through decode() visits init_context() 61 | Decoded = router_decoder_custom_worker:decode(Pid, Payload, Port, Uplink), 62 | ?assertMatch({js_error, <<"ReferenceError:", _/binary>>}, Decoded), 63 | 64 | %% While were here, confirm abbreviated message (which gets used in log entry) 65 | {js_error, <<"ReferenceError:", OmitFullStackTrace/binary>>} = Decoded, 66 | ?assertMatch(nomatch, binary:match(OmitFullStackTrace, <<"\n">>)), 67 | 68 | %% Subsequent trips through decode/4 should avoid calling V8 69 | ?assertMatch( 70 | {error, ignoring_invalid_javascript}, 71 | router_decoder_custom_worker:decode(Pid, Payload, Port, Uplink) 72 | ), 73 | ?assertMatch( 74 | {error, ignoring_invalid_javascript}, 75 | router_decoder_custom_worker:decode(Pid, Payload, Port, Uplink) 76 | ), 77 | ?assertMatch( 78 | {error, ignoring_invalid_javascript}, 79 | router_decoder_custom_worker:decode(Pid, Payload, Port, Uplink) 80 | ), 81 | 82 | MalformedJS1 = <<"funct Decoder(a,b,c) { return 42; }">>, 83 | Args1 = #{ 84 | id => <<"decoder_id1">>, 85 | hash => <<"badca112">>, 86 | function => MalformedJS1, 87 | vm => V8VM 88 | }, 89 | %% Worker gets created regardless of an error, because it will be 90 | %% less overhead overall (compared to rejecting and having next 91 | %% packet start fresh). 92 | {ok, Pid1} = router_decoder_custom_worker:start_link(Args1), 93 | %% Now, first call to decode gets ignored 94 | ?assertMatch( 95 | {error, ignoring_invalid_javascript}, 96 | router_decoder_custom_worker:decode(Pid1, Payload, Port, Uplink) 97 | ), 98 | 99 | %% Cleanup: 100 | %% skipping: gen_server:stop(Pid), 101 | %% skipping: gen_server:stop(Pid1), 102 | gen_server:stop(VM), 103 | gen_server:stop(V8VM), 104 | ok. 105 | -------------------------------------------------------------------------------- /test/router_discovery_handler_test.erl: -------------------------------------------------------------------------------- 1 | -module(router_discovery_handler_test). 2 | 3 | -behavior(libp2p_framed_stream). 4 | 5 | %% ------------------------------------------------------------------ 6 | %% API Function Exports 7 | %% ------------------------------------------------------------------ 8 | 9 | -export([ 10 | server/4, 11 | client/2 12 | ]). 13 | 14 | %% ------------------------------------------------------------------ 15 | %% libp2p_framed_stream Function Exports 16 | %% ------------------------------------------------------------------ 17 | -export([ 18 | init/3, 19 | handle_data/3, 20 | handle_info/3 21 | ]). 22 | 23 | -record(state, {forward :: pid() | undefined}). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% API Function Definitions 27 | %% ------------------------------------------------------------------ 28 | server(Connection, Path, _TID, Args) -> 29 | lager:info("starting server with ~p", [{Connection, Path, _TID, Args}]), 30 | libp2p_framed_stream:server(?MODULE, Connection, Args). 31 | 32 | client(Connection, Args) -> 33 | lager:info("starting client with ~p", [{Connection, Args}]), 34 | libp2p_framed_stream:client(?MODULE, Connection, Args). 35 | 36 | %% ------------------------------------------------------------------ 37 | %% libp2p_framed_stream Function Definitions 38 | %% ------------------------------------------------------------------ 39 | 40 | init(client, _Conn, _Args) -> 41 | lager:info("client started with ~p", [_Args]), 42 | {ok, #state{}}; 43 | init(server, _Conn, _Args) -> 44 | lager:info("server started with ~p", [_Args]), 45 | [Pid] = _Args, 46 | {ok, #state{forward = Pid}}. 47 | 48 | handle_data(server, Data, #state{forward = Pid} = State) -> 49 | lager:info("server got data ~p", [Data]), 50 | Pid ! {?MODULE, Data}, 51 | {noreply, State}; 52 | handle_data(_Type, _Data, State) -> 53 | lager:warning("~p got data ~p", [_Type, _Data]), 54 | {noreply, State}. 55 | 56 | handle_info(_Type, _Msg, State) -> 57 | lager:warning("test ~p got info ~p", [_Type, _Msg]), 58 | {noreply, State}. 59 | 60 | %% ------------------------------------------------------------------ 61 | %% Internal Function Definitions 62 | %% ------------------------------------------------------------------ 63 | -------------------------------------------------------------------------------- /test/router_handler_test.erl: -------------------------------------------------------------------------------- 1 | -module(router_handler_test). 2 | 3 | -behavior(libp2p_framed_stream). 4 | 5 | %% ------------------------------------------------------------------ 6 | %% API Function Exports 7 | %% ------------------------------------------------------------------ 8 | 9 | -export([ 10 | server/4, 11 | client/2, 12 | version/0 13 | ]). 14 | 15 | %% ------------------------------------------------------------------ 16 | %% libp2p_framed_stream Function Exports 17 | %% ------------------------------------------------------------------ 18 | -export([ 19 | init/3, 20 | handle_data/3, 21 | handle_info/3 22 | ]). 23 | 24 | -ifdef(TEST). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | -endif. 27 | 28 | -record(state, { 29 | pid :: pid(), 30 | key = undefined :: binary() | undefined 31 | }). 32 | 33 | %% ------------------------------------------------------------------ 34 | %% API Function Definitions 35 | %% ------------------------------------------------------------------ 36 | server(Connection, Path, _TID, Args) -> 37 | libp2p_framed_stream:server(?MODULE, Connection, [Path | Args]). 38 | 39 | client(Connection, Args) -> 40 | libp2p_framed_stream:client(?MODULE, Connection, Args). 41 | 42 | -spec version() -> string(). 43 | version() -> 44 | "simple_http/1.0.0". 45 | 46 | %% ------------------------------------------------------------------ 47 | %% libp2p_framed_stream Function Definitions 48 | %% ------------------------------------------------------------------ 49 | 50 | init(server, _Conn, _Args) -> 51 | lager:info("server started with ~p", [_Args]), 52 | {ok, #state{}}; 53 | init(client, _Conn, [Pid] = _Args) -> 54 | lager:info("client started with ~p", [_Args]), 55 | {ok, #state{pid = Pid}}; 56 | init(client, _Conn, [Pid, Pubkeybin] = _Args) -> 57 | lager:info("client started with ~p", [_Args]), 58 | {ok, #state{pid = Pid, key = Pubkeybin}}. 59 | 60 | handle_data(client, Data, #state{pid = Pid, key = undefined} = State) -> 61 | Pid ! {client_data, undefined, Data}, 62 | {noreply, State}; 63 | handle_data(client, Data, #state{pid = Pid, key = Pubkeybin} = State) -> 64 | Pid ! {client_data, Pubkeybin, Data}, 65 | {noreply, State}; 66 | handle_data(_Type, _Data, State) -> 67 | lager:warning("~p got data ~p", [_Type, _Data]), 68 | {noreply, State}. 69 | 70 | handle_info(client, {send, Data}, State) -> 71 | {noreply, State, Data}; 72 | handle_info(_Type, _Msg, State) -> 73 | lager:warning("test ~p got info ~p", [_Type, _Msg]), 74 | {noreply, State}. 75 | 76 | %% ------------------------------------------------------------------ 77 | %% Internal Function Definitions 78 | %% ------------------------------------------------------------------ 79 | 80 | %% ------------------------------------------------------------------ 81 | %% EUNIT Tests 82 | %% ------------------------------------------------------------------ 83 | -ifdef(TEST). 84 | -endif. 85 | -------------------------------------------------------------------------------- /test/router_ics_gateway_location_worker_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_ics_gateway_location_worker_SUITE). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("../src/grpc/autogen/iot_config_pb.hrl"). 5 | -include("console_test.hrl"). 6 | 7 | -export([ 8 | all/0, 9 | groups/0, 10 | init_per_testcase/2, 11 | end_per_testcase/2, 12 | init_per_group/2, 13 | end_per_group/2 14 | ]). 15 | 16 | -export([ 17 | main_test/1 18 | ]). 19 | 20 | -record(location, { 21 | gateway :: libp2p_crypto:pubkey_bin(), 22 | timestamp :: non_neg_integer(), 23 | h3_index :: h3:index() 24 | }). 25 | 26 | %%-------------------------------------------------------------------- 27 | %% COMMON TEST CALLBACK FUNCTIONS 28 | %%-------------------------------------------------------------------- 29 | 30 | %%-------------------------------------------------------------------- 31 | %% @public 32 | %% @doc 33 | %% Running tests for this suite 34 | %% @end 35 | %%-------------------------------------------------------------------- 36 | all() -> 37 | [ 38 | {group, chain_alive}, 39 | {group, chain_dead} 40 | ]. 41 | 42 | groups() -> 43 | [ 44 | {chain_alive, [main_test]}, 45 | {chain_dead, [main_test]} 46 | ]. 47 | 48 | %%-------------------------------------------------------------------- 49 | %% TEST CASE SETUP 50 | %%-------------------------------------------------------------------- 51 | init_per_group(GroupName, Config) -> 52 | test_utils:init_per_group(GroupName, Config). 53 | 54 | init_per_testcase(TestCase, Config) -> 55 | persistent_term:put(router_test_ics_gateway_service, self()), 56 | test_utils:init_per_testcase(TestCase, Config). 57 | 58 | %%-------------------------------------------------------------------- 59 | %% TEST CASE TEARDOWN 60 | %%-------------------------------------------------------------------- 61 | end_per_group(GroupName, Config) -> 62 | test_utils:end_per_group(GroupName, Config). 63 | 64 | end_per_testcase(TestCase, Config) -> 65 | test_utils:end_per_testcase(TestCase, Config), 66 | ok. 67 | 68 | %%-------------------------------------------------------------------- 69 | %% TEST CASES 70 | %%-------------------------------------------------------------------- 71 | 72 | main_test(_Config) -> 73 | #{public := PubKey1} = libp2p_crypto:generate_keys(ecc_compact), 74 | PubKeyBin1 = libp2p_crypto:pubkey_to_bin(PubKey1), 75 | ExpectedIndex = h3:from_string("8828308281fffff"), 76 | ok = router_test_ics_gateway_service:register_gateway_location( 77 | PubKeyBin1, 78 | "8828308281fffff" 79 | ), 80 | 81 | Before = erlang:system_time(millisecond), 82 | 83 | %% Let worker start 84 | ok = test_utils:wait_until(fun() -> 85 | try router_ics_gateway_location_worker:get(PubKeyBin1) of 86 | {ok, ExpectedIndex} -> true; 87 | _ -> false 88 | catch 89 | _:_ -> 90 | false 91 | end 92 | end), 93 | 94 | [LocationRec] = ets:lookup(router_ics_gateway_location_worker_ets, PubKeyBin1), 95 | 96 | ?assertEqual(PubKeyBin1, LocationRec#location.gateway), 97 | ?assertEqual(ExpectedIndex, LocationRec#location.h3_index), 98 | 99 | Timestamp = LocationRec#location.timestamp, 100 | Now = erlang:system_time(millisecond), 101 | 102 | ?assert(Timestamp > Before), 103 | ?assert(Timestamp =< Now), 104 | 105 | [{location, Req1}] = rcv_loop([]), 106 | ?assertEqual(PubKeyBin1, Req1#iot_config_gateway_location_req_v1_pb.gateway), 107 | 108 | ok. 109 | 110 | %% ------------------------------------------------------------------ 111 | %% Helper functions 112 | %% ------------------------------------------------------------------ 113 | 114 | rcv_loop(Acc) -> 115 | receive 116 | {router_test_ics_gateway_service, Type, Req} -> 117 | lager:notice("got router_test_ics_gateway_service ~p req ~p", [Type, Req]), 118 | rcv_loop([{Type, Req} | Acc]) 119 | after timer:seconds(2) -> Acc 120 | end. 121 | -------------------------------------------------------------------------------- /test/router_metrics_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_metrics_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | groups/0, 6 | init_per_testcase/2, 7 | end_per_testcase/2, 8 | init_per_group/2, 9 | end_per_group/2 10 | ]). 11 | 12 | -export([ 13 | metrics_test/1 14 | ]). 15 | 16 | -include_lib("helium_proto/include/blockchain_state_channel_v1_pb.hrl"). 17 | -include_lib("common_test/include/ct.hrl"). 18 | -include_lib("eunit/include/eunit.hrl"). 19 | 20 | -include("metrics.hrl"). 21 | 22 | %%-------------------------------------------------------------------- 23 | %% COMMON TEST CALLBACK FUNCTIONS 24 | %%-------------------------------------------------------------------- 25 | 26 | %%-------------------------------------------------------------------- 27 | %% @public 28 | %% @doc 29 | %% Running tests for this suite 30 | %% @end 31 | %%-------------------------------------------------------------------- 32 | all() -> 33 | [ 34 | {group, chain_alive}, 35 | {group, chain_dead} 36 | ]. 37 | 38 | groups() -> 39 | [ 40 | {chain_alive, all_tests()}, 41 | {chain_dead, all_tests()} 42 | ]. 43 | 44 | all_tests() -> 45 | [ 46 | metrics_test 47 | ]. 48 | 49 | %%-------------------------------------------------------------------- 50 | %% TEST CASE SETUP 51 | %%-------------------------------------------------------------------- 52 | init_per_group(GroupName, Config) -> 53 | test_utils:init_per_group(GroupName, Config). 54 | 55 | init_per_testcase(TestCase, Config) -> 56 | test_utils:init_per_testcase(TestCase, Config). 57 | 58 | %%-------------------------------------------------------------------- 59 | %% TEST CASE TEARDOWN 60 | %%-------------------------------------------------------------------- 61 | end_per_group(GroupName, Config) -> 62 | test_utils:end_per_group(GroupName, Config). 63 | 64 | end_per_testcase(TestCase, Config) -> 65 | test_utils:end_per_testcase(TestCase, Config). 66 | 67 | %%-------------------------------------------------------------------- 68 | %% TEST CASES 69 | %%-------------------------------------------------------------------- 70 | 71 | metrics_test(Config) -> 72 | #{ 73 | pubkey_bin := _PubKeyBin, 74 | stream := _Stream, 75 | hotspot_name := _HotspotName 76 | } = test_utils:join_device(Config), 77 | 78 | router_metrics ! ?METRICS_TICK, 79 | ok = timer:sleep(timer:seconds(1)), 80 | 81 | {_, RoutingPacketTime} = prometheus_histogram:value(?METRICS_ROUTING_PACKET, [ 82 | join, 83 | accepted, 84 | accepted, 85 | true 86 | ]), 87 | %% Minimum of 2s per but it should not take more than 25ms 88 | ct:pal("[~p:~p:~p] MARKER ~p~n", [?MODULE, ?FUNCTION_NAME, ?LINE, RoutingPacketTime]), 89 | ?assert(RoutingPacketTime > 1999 andalso RoutingPacketTime < 2025), 90 | 91 | {_, ConsoleAPITime} = prometheus_histogram:value(?METRICS_CONSOLE_API, [ 92 | report_status, ok 93 | ]), 94 | ?assert(ConsoleAPITime < 100), 95 | 96 | ?assertEqual(true, prometheus_boolean:value(?METRICS_WS)), 97 | 98 | ?assert(prometheus_gauge:value(?METRICS_VM_CPU, [1]) > 0), 99 | 100 | ok. 101 | -------------------------------------------------------------------------------- /test/router_sc_worker_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_sc_worker_SUITE). 2 | 3 | -include("metrics.hrl"). 4 | -include_lib("common_test/include/ct.hrl"). 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | -export([ 8 | all/0, 9 | init_per_testcase/2, 10 | end_per_testcase/2 11 | ]). 12 | 13 | -export([ 14 | sc_worker_test/1 15 | ]). 16 | 17 | -record(state, { 18 | pubkey :: libp2p_crypto:public_key(), 19 | sig_fun :: libp2p_crypto:sig_fun(), 20 | oui = undefined :: undefined | non_neg_integer(), 21 | is_active = false :: boolean(), 22 | tref = undefined :: undefined | reference(), 23 | in_flight = [] :: [blockchain_txn_state_channel_open_v1:id()], 24 | open_sc_limit = undefined :: undefined | non_neg_integer() 25 | }). 26 | 27 | %%-------------------------------------------------------------------- 28 | %% COMMON TEST CALLBACK FUNCTIONS 29 | %%-------------------------------------------------------------------- 30 | 31 | %%-------------------------------------------------------------------- 32 | %% @public 33 | %% @doc 34 | %% Running tests for this suite 35 | %% @end 36 | %%-------------------------------------------------------------------- 37 | all() -> 38 | [ 39 | sc_worker_test 40 | ]. 41 | 42 | %%-------------------------------------------------------------------- 43 | %% TEST CASE SETUP 44 | %%-------------------------------------------------------------------- 45 | init_per_testcase(TestCase, Config) -> 46 | test_utils:init_per_testcase(TestCase, Config). 47 | 48 | %%-------------------------------------------------------------------- 49 | %% TEST CASE TEARDOWN 50 | %%-------------------------------------------------------------------- 51 | end_per_testcase(TestCase, Config) -> 52 | test_utils:end_per_testcase(TestCase, Config). 53 | 54 | %%-------------------------------------------------------------------- 55 | %% TEST CASES 56 | %%-------------------------------------------------------------------- 57 | 58 | sc_worker_test(Config) -> 59 | {ok, _} = lager:trace_console( 60 | [{module, blockchain_state_channels_server}], 61 | debug 62 | ), 63 | ConsensusMembers = proplists:get_value(consensus_member, Config), 64 | 65 | % Wait until sc worker ative 66 | test_utils:wait_until(fun() -> 67 | State = sys:get_state(router_sc_worker), 68 | State#state.is_active 69 | end), 70 | 71 | % Mock submit_txn to create/gossip block 72 | meck:new(blockchain_worker, [passthrough, no_history]), 73 | meck:expect(blockchain_worker, submit_txn, fun(Txn, Callback) -> 74 | case blockchain_test_utils:create_block(ConsensusMembers, [Txn]) of 75 | {error, _Reason} = Error -> 76 | Callback(Error); 77 | {ok, Block} -> 78 | _ = blockchain_test_utils:add_block( 79 | Block, 80 | self(), 81 | blockchain_swarm:tid() 82 | ), 83 | Callback(ok) 84 | end, 85 | ok 86 | end), 87 | 88 | _ = test_utils:add_oui(Config), 89 | 90 | % Force tick to open first SC 91 | router_sc_worker ! '__router_sc_tick', 92 | ok = test_utils:wait_until(fun() -> {ok, 3} == router_blockchain:height() end), 93 | 94 | % We should have 1 SC and it should be active 95 | ?assertEqual(1, maps:size(blockchain_state_channels_server:get_all())), 96 | ?assertEqual(1, blockchain_state_channels_server:get_actives_count()), 97 | 98 | % Force tick to open another SC 99 | router_sc_worker ! '__router_sc_tick', 100 | ok = test_utils:wait_until(fun() -> {ok, 4} == router_blockchain:height() end), 101 | 102 | % Now we should have 1 active and 2 opened SC, it gives us some room just in case 103 | ?assertEqual(2, maps:size(blockchain_state_channels_server:get_all())), 104 | ?assertEqual(1, blockchain_state_channels_server:get_actives_count()), 105 | 106 | % Force tick again, the first SC is getting close to expire so lets open more 107 | router_sc_worker ! '__router_sc_tick', 108 | ok = test_utils:wait_until(fun() -> {ok, 5} == router_blockchain:height() end), 109 | 110 | ?assertEqual(3, maps:size(blockchain_state_channels_server:get_all())), 111 | ?assertEqual(1, blockchain_state_channels_server:get_actives_count()), 112 | 113 | % Force tick again, nothing should happen now 114 | router_sc_worker ! '__router_sc_tick', 115 | timer:sleep(1000), 116 | 117 | ?assertEqual(3, maps:size(blockchain_state_channels_server:get_all())), 118 | ?assertEqual(1, blockchain_state_channels_server:get_actives_count()), 119 | State = sys:get_state(router_sc_worker), 120 | ?assertEqual(0, erlang:length(State#state.in_flight)), 121 | 122 | ?assert(meck:validate(blockchain_worker)), 123 | meck:unload(blockchain_worker), 124 | 125 | ok. 126 | 127 | %% ------------------------------------------------------------------ 128 | %% Helper functions 129 | %% ------------------------------------------------------------------ 130 | -------------------------------------------------------------------------------- /test/router_test_ics_gateway_service.erl: -------------------------------------------------------------------------------- 1 | -module(router_test_ics_gateway_service). 2 | 3 | -behaviour(helium_iot_config_gateway_bhvr). 4 | -include("../src/grpc/autogen/iot_config_pb.hrl"). 5 | 6 | -export([ 7 | init/2, 8 | handle_info/2 9 | ]). 10 | 11 | -export([ 12 | region_params/2, 13 | load_region/2, 14 | location/2, 15 | info/2, 16 | info_stream/2 17 | ]). 18 | 19 | -export([register_gateway_location/2]). 20 | 21 | -spec init(atom(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). 22 | init(_RPC, StreamState) -> 23 | StreamState. 24 | 25 | -spec handle_info(Msg :: any(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). 26 | handle_info(_Msg, StreamState) -> 27 | StreamState. 28 | 29 | region_params(_Ctx, _Msg) -> 30 | {grpc_error, {12, <<"UNIMPLEMENTED">>}}. 31 | 32 | load_region(_Ctx, _Msg) -> 33 | {grpc_error, {12, <<"UNIMPLEMENTED">>}}. 34 | 35 | info(_Ctx, _Msg) -> 36 | {grpc_error, {12, <<"UNIMPLEMENTED">>}}. 37 | 38 | info_stream(_Ctx, _Msg) -> 39 | {grpc_error, {12, <<"UNIMPLEMENTED">>}}. 40 | 41 | location(Ctx, Req) -> 42 | case verify_location_req(Req) of 43 | true -> 44 | PubKeyBin = Req#iot_config_gateway_location_req_v1_pb.gateway, 45 | case maybe_get_registered_location(PubKeyBin) of 46 | {ok, Location} -> 47 | lager:info("got location req ~p", [Req]), 48 | Res = #iot_config_gateway_location_res_v1_pb{ 49 | %% location = "8c29a962ed5b3ff" 50 | location = Location 51 | }, 52 | catch persistent_term:get(?MODULE) ! {?MODULE, location, Req}, 53 | {ok, Res, Ctx}; 54 | {error, not_found} -> 55 | %% 5, not_found 56 | {grpc_error, {grpcbox_stream:code_to_status(5), <<"gateway not asserted">>}} 57 | end; 58 | false -> 59 | lager:error("failed to verify location req ~p", [Req]), 60 | {grpc_error, {7, <<"PERMISSION_DENIED">>}} 61 | end. 62 | 63 | -spec verify_location_req(Req :: #iot_config_gateway_location_req_v1_pb{}) -> boolean(). 64 | verify_location_req(Req) -> 65 | EncodedReq = iot_config_pb:encode_msg( 66 | Req#iot_config_gateway_location_req_v1_pb{ 67 | signature = <<>> 68 | }, 69 | iot_config_gateway_location_req_v1_pb 70 | ), 71 | libp2p_crypto:verify( 72 | EncodedReq, 73 | Req#iot_config_gateway_location_req_v1_pb.signature, 74 | libp2p_crypto:bin_to_pubkey(router_blockchain:pubkey_bin()) 75 | ). 76 | 77 | -spec register_gateway_location( 78 | PubKeyBin :: libp2p_crypto:pubkey_bin(), 79 | Location :: string() 80 | ) -> ok. 81 | register_gateway_location(PubKeyBin, Location) -> 82 | Map = persistent_term:get(known_locations, #{}), 83 | ok = persistent_term:put(known_locations, Map#{PubKeyBin => Location}). 84 | 85 | -spec maybe_get_registered_location(PubKeyBin :: libp2p_crypto:pubkey_bin()) -> 86 | {ok, string()} | {error, not_found}. 87 | maybe_get_registered_location(PubKeyBin) -> 88 | Map = persistent_term:get(known_locations, #{}), 89 | case maps:get(PubKeyBin, Map, undefined) of 90 | undefined -> {error, not_found}; 91 | Location -> {ok, Location} 92 | end. 93 | 94 | %% NOTE: if more asserted gateways are needed, use these locations. 95 | %% location = "8828308281fffff", %% original from location worker 96 | %% location = "8c29a962ed5b3ff" %% from blockchain init 97 | %% 98 | %% all locations inserted into chain 99 | %% ["8C29A962ED5B3FF","8C29A975818B3FF","8C29A97497733FF", 100 | %% "8C29A92809AEDFF","8C29A92A98DE7FF","8C29A92E404ABFF", 101 | %% "8C29A92552F31FF","8C29A924C86E7FF","8C2834535A1B5FF", 102 | %% "8C2834CD22653FF","8C2834CCE41C3FF","8C28341B06945FF"] 103 | -------------------------------------------------------------------------------- /test/router_v8_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(router_v8_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_testcase/2, 6 | end_per_testcase/2 7 | ]). 8 | 9 | -export([ 10 | invalid_context_test/1 11 | ]). 12 | 13 | -include_lib("common_test/include/ct.hrl"). 14 | -include_lib("eunit/include/eunit.hrl"). 15 | 16 | %%-------------------------------------------------------------------- 17 | %% @public 18 | %% @doc 19 | %% Running tests for this suite 20 | %% @end 21 | %%-------------------------------------------------------------------- 22 | all() -> 23 | [invalid_context_test]. 24 | 25 | %%-------------------------------------------------------------------- 26 | %% TEST CASE SETUP 27 | %%-------------------------------------------------------------------- 28 | init_per_testcase(TestCase, Config) -> 29 | test_utils:init_per_testcase(TestCase, Config). 30 | 31 | %%-------------------------------------------------------------------- 32 | %% TEST CASE TEARDOWN 33 | %%-------------------------------------------------------------------- 34 | end_per_testcase(TestCase, Config) -> 35 | test_utils:end_per_testcase(TestCase, Config). 36 | 37 | %%-------------------------------------------------------------------- 38 | %% TEST CASES 39 | %%-------------------------------------------------------------------- 40 | 41 | invalid_context_test(_Config) -> 42 | GoodFunction = 43 | << 44 | "function Decoder(bytes, port) {\n" 45 | " var payload = {\"Testing\": \"42\"};\n" 46 | " return payload;\n" 47 | "}" 48 | >>, 49 | BadFunction = <<"function Decoder() { returrrrrrrn 0; }">>, 50 | 51 | VMPid = 52 | case router_v8:start_link(#{}) of 53 | {ok, Pid} -> Pid; 54 | {error, {already_started, Pid}} -> Pid 55 | end, 56 | {ok, VM} = router_v8:get(), 57 | {ok, Context1} = erlang_v8:create_context(VM), 58 | {ok, Context2} = erlang_v8:create_context(VM), 59 | ?assertNotMatch(Context1, Context2), 60 | 61 | Payload = erlang:binary_to_list(base64:decode(<<"H4Av/xACRU4=">>)), 62 | Port = 6, 63 | Result = #{<<"Testing">> => <<"42">>}, 64 | 65 | %% Eval good function and ensure function works more than once 66 | ?assertMatch({ok, undefined}, erlang_v8:eval(VM, Context1, GoodFunction)), 67 | ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), 68 | ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), 69 | 70 | %% Call undefined function 71 | ?assertMatch( 72 | {error, <<"ReferenceError: Decoder is not defined", _/binary>>}, 73 | erlang_v8:call(VM, Context2, <<"Decoder">>, [Payload, Port]) 74 | ), 75 | 76 | %% First Context still works 77 | ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), 78 | 79 | %% Eval bad function 80 | ?assertMatch({error, crashed}, erlang_v8:eval(VM, Context2, BadFunction)), 81 | 82 | %% Upon an error (other than invalid_source_size), v8 Port gets 83 | %% killed and restarted 84 | ?assertMatch(true, erlang:is_process_alive(VMPid)), 85 | 86 | %% Unintuitively, we get invalid context when attempting to reuse first Context: 87 | ?assertMatch( 88 | {error, invalid_context}, 89 | erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port]) 90 | ), 91 | %% But that's because set of Context needs to be repopulated within V8's VM: 92 | erlang_v8:restart_vm(VM), 93 | 94 | %% First Context no longer works-- not ideal behavior but what we 95 | %% have at hand. Ideally, return value would still match `Result'. 96 | ?assertMatch( 97 | {error, invalid_context}, 98 | erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port]) 99 | ), 100 | 101 | gen_server:stop(VMPid), 102 | ok. 103 | --------------------------------------------------------------------------------