Skip to content

Commit

Permalink
Lora + build artifacts as releases (#26)
Browse files Browse the repository at this point in the history
* Running draft of Lora gateway implementation

* Lora bridge: improve code style and eliminate warnings

* move mkEnv and readArtifacts to a helper module in DApp.Support, cleanup redundant imports from the refactor

* make DApp.Support functions MonadAff because its easy and clean

* make fromMaybe ... PACKET_RECEIVER_PORT more readable

* add support for estimating gas of relay Txs

* export estimate utils, add to AppEnv

* Lora: dry run messages and logging

* Improvements to code and error handling

* Add PI installation documentation

* Improve PI documentation

* Added upload of foam.lite.tgz to travis job

Co-authored-by: Leo Lara <leo@leolara.me>
Co-authored-by: Ilya Ostrovskiy <iostat@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 1, 2022
1 parent 03b8659 commit e4567ea
Show file tree
Hide file tree
Showing 17 changed files with 969 additions and 266 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ dist/*
deployment/keys.txt
foam5g.tgz
package-lock.json
foam.lite.tgz
24 changes: 15 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ script:
- cp -v .deployed-contracts/*.json build/
- make bundle
deploy:
provider: pages
local-dir: ./dist/
email: deploy@travis-ci.com
name: Deployment Bot
skip-cleanup: true
github-token: $GITHUB_TOKEN
keep-history: true
on:
branch: main
- provider: pages
local-dir: ./dist/
email: deploy@travis-ci.com
name: Deployment Bot
skip-cleanup: true
github-token: $GITHUB_TOKEN
keep-history: true
on:
branch: main
- provider: releases
token: $GITHUB_TOKEN
file: "foam.lite.tgz"
skip_cleanup: true
on:
tags: true
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ bundle: ## Bundle the app for distribution
mkdir -p dist/build
cp build/*.json dist/build
npm run bundle-app
npm run bundle-server
npm run bundle-lora
npm run webpack:prod
npm run backend-dist
mv dist foam.lite
Expand Down
180 changes: 180 additions & 0 deletions PI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Running Foam.lite bridge on a Raspberry Pi 4 RAK Lora hat

In this documentation we will show how to set up and operate a Foam lite gateway using a Raspberry Pi 4 and a RAK Lora hat. We will see:

+ How to connect the hardware
+ How to install the Lora RAK drivers and Foam lite gateway software
+ How to check the installation is working

After this process you will have a usable Foam Lite lora gateway.

## Hardware installation

Our system is composed of two main components:
+ Raspberry Pi 4, an ARM based computer
+ RAK lora Hat, a lora network interface that connects with the Raspberry Pi through an interface called GPIO (https://www.hwlibre.com/en/gpio-raspberry-pi/)

Connect the Lora hat on the Raspberry Pi GPIO connector in the position that the hat stays over the Pi. The Hat will probably come with riser bolts, you can use them to safely lock the hat in place.

After that, you should connect the antenas to the RAK Lora hat

[Insert photo with result]

After this process the Pi has the capability to communicate with the Lora network.

## Software installation

### Install Linux distribution

Install the latest Raspberry Pi OS Lite from https://www.raspberrypi.org/software/operating-systems/#raspberry-pi-os-32-bit into a SD card that you can insert into the Pi.

### Install RAK drivers

RAK, the manufacturer of the Lora hat, has a driver that runs on Raspberry Pi OS and is able to communicate with the Lora network, and convert Lora packets into UDP packets that will be sent to our Foam Lite Lora gateway. First we need to install this driver.

Follow the instructions here: https://github.com/RAKWireless/rak_common_for_gateway#installation-procedure

When configuring the gateway with `gateway-config` select TTN and the right Lora standard.

Then edit (with sudo) `/opt/ttn-gateway/packet_forwarder/lora_pkt_fwd/global_conf.json`:
+ in `gateway_conf.server_address` make sure it is 127.0.0.1.
+ in `gateway_conf.serv_port_up` and `gateway_conf.serv_port_down` set the port where you are going to run foam.lite bridge, for example 7000

### Compile Foam.lite gateway

Foam.lite gateway is written in purescript a language that compiles to node/JS, at this moment it is not possible to compile purescript directly on the raspberry Pi. Hence, we should compile it to Node/JS on a desktop computer and

In your desktop, install node 14, purescript 0.13.8 and spago.

Clone https://github.com/f-o-a-m/foam.lite , run `npm i` and then run `npm run bundle-lora`.


### Install Foam.lite gateway into

We should prepare the Pi to be able to run the software we have compiled on the desktop computer.

In the Pi, run:

```
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
$ export NVM_DIR="$HOME/.nvm"
$ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
$ [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
$ nvm install 14
$ nvm use 14
```

Copy the whole project where you compiled Foam.lite on your desktop machine to the Pi. For example using `rsync -av` over ssh

## Running Foam.lite manually

You could run manually foam.lite lora bridge, assuming that you have copied the project in the Pi device to ~/lora, follow this instructions.

```
$ cd lora
$ export NODE_URL=http://ethnode:8545 # point it to your ethereum node
$ export PACKET_RECEIVER_PORT=7000 # use the same port that you set up in /opt/ttn-gateway/packet_forwarder/lora_pkt_fwd/global_conf.json
$ node dist/lora.js
```

In another terminal:

```
$ sudo service ttn-gateway restart
```

## Running Foam.lite with systemd

Raspberry Pi OS uses systemd to bootstrap different services that run on the Pi. We can set up systemd so we keep running our Foam.lite lora gateway service.

As root add the file /etc/systemd/system/foam.lite.service, with the following content:

```
[Unit]
Description=Foam Lite lora gateway
[Service]
WorkingDirectory=/home/pi/foam
ExecStart=/home/pi/.nvm/versions/node/v14.18.1/bin/node dist/lora.js
Restart=always
User=pi
Environment="NODE_URL=http://ethnode:8545"
Environment="PACKET_RECEIVER_PORT=7000"
[Install]
WantedBy=multi-user.target
```

Adapting the working directory, node binary, NODE_URL and PACKET_RECEIVER_PORT to your set-up.

Then run,

```
$ sudo systemctl daemon-reload
$ sudo systemctl enable foam.lite
```

Foam.lite lora gateway will then be running always as a systems service on your Pi. You can check it logs with `journalctl -f -u foam.lite`.

## Testing the system

### Sending a invalid message

First thing we can do to check that the gateway is listening for Lora packages, we can send any non-foam lora package and see that the gateway is try to decode it. For example, observe the logs with `journalctl -f -u foam.lite` and send from a lora device the package 0x686F6C61

You should see this on the logs:

```
[INFO] (PushDataJSON { rxpk: (Just [(ReceivedPacket { data: (Base64Encoded "AGhvbGE=") })]) })
[WARN] PUSH DATA packet payload cannot be decoded as a FOAM message
```

The gateway has received the lora package and has acknowledged that it is not a Foam package.

### Minting from a lora device

For last we should check that if we send a Foam package that mints an NFT, that is processed correctly.

Assuming we have the Ethereum node set up and the contracts deployed, we should do the following on our Foam.lite project at our desktop:

```
make bundle
# Make an account that doesn't exist on the Ethereum node
PRIVATE_KEY=$(node dist/helper.js generate-private-key)
echo "The private key is ${PRIVATE_KEY}"
# Faucet the account with some of the FungibleToken we deployed, as well
# as some Ether to let it approve the RelayableNFT contract to spend its FungibleTokens
node dist/helper.js faucet --to-private-key ${PRIVATE_KEY} --also-ether
# Approve the RelayableNFT token to spend the account's balance (and automatically execute the approve() transaction on-chain)
node dist/helper.js approve --private-key ${PRIVATE_KEY} --submit --node-url "http://localhost:8545"
# Double check our balances
node dist/helper.js balance --private-key ${PRIVATE_KEY}
# Make a minting message that creates an "arbitrary" hello-world message
# Using relay-nonce 0, and paying 0.1 FT to the relayer
node dist/helper.js sign-mint --private-key ${PRIVATE_KEY} -a 'Hello World!' --nonce $(node dist/helper.js nonce -p ${PRIVATE_KEY}) --fee-amount 1000000000000000000
```

The last command will print in the console an hexadecimal string that contains the valid Lora message to ming an NFT, for example: `195955fd323a05f96e2ef8c3ae29f811e5de87059379cf964304e7233b1b8998592a7cdecd027ee93440e2a6cec877ea4217db28e9e3fc365d73c9daea5004921c0000000000000000000000000de0b6b3a76400000e413a48656c6c6f20576f726c6421`

We should observe the gateway logs and send this from our lora device. In the logs we will see something like:

```
[DEBUG] received UDP packet from 127.0.0.1:38845
[INFO] "\2|\19\0d_\1\127~\\)Z{\"rxpk\":[{\"tmst\":2066085059,\"time\":\"2021-11-24T15:55:00.158604Z\",\"tmms\":1321804519158,\"chan\":1,\"rfch\":1,\"freq\":868.300000,\"stat\":1,\"modu\":\"LORA\",\"datr\":\"SF7BW125\",\"codr\":\"4/5\",\"lsnr\":9.0,\"rssi\":-22,\"size\":101,\"data\":\"ABlZVf0yOgX5bi74w64p+BHl3ocFk3nPlkME5yM7G4mYWSp83s0Cfuk0QOKmzsh36kIX2yjp4/w2XXPJ2upQBJIcAAAAAAAAAAAAAAAADeC2s6dkAAAOQTpIZWxsbyBXb3JsZCE=\"}]}"
[INFO] PUSH_DATA
[INFO] (PushDataJSON { rxpk: (Just [(ReceivedPacket { data: (Base64Encoded "ABlZVf0yOgX5bi74w64p+BHl3ocFk3nPlkME5yM7G4mYWSp83s0Cfuk0QOKmzsh36kIX2yjp4/w2XXPJ2upQBJIcAAAAAAAAAAAAAAAADeC2s6dkAAAOQTpIZWxsbyBXb3JsZCE=") })]) })
[DEBUG] sending packet
[INFO] relaying message (DecodedMint { feeAmount: 1000000000000000000, nonce: 0, signature: (Signature { r: 0x195955fd323a05f96e2ef8c3ae29f811e5de87059379cf964304e7233b1b8998, s: 0x592a7cdecd027ee93440e2a6cec877ea4217db28e9e3fc365d73c9daea500492, v: 28 }), tokenData: (pack [(mkQuotient 65),(mkQuotient 58),(mkQuotient 72),(mkQuotient 101),(mkQuotient 108),(mkQuotient 108),(mkQuotient 111),(mkQuotient 32),(mkQuotient 87),(mkQuotient 111),(mkQuotient 114),(mkQuotient 108),(mkQuotient 100),(mkQuotient 33)]) })
Nov 24 15:54:38 rak-gateway node[5725]: 2021-11-24T15:54:38.816Z [DEBUG] doing dry run of (DecodedMint { feeAmount: 1000000000000000000, nonce: 0, signature: (Signature { r: 0x195955fd323a05f96e2ef8c3ae29f811e5de87059379cf964304e7233b1b8998, s: 0x592a7cdecd027ee93440e2a6cec877ea4217db28e9e3fc365d73c9daea500492, v: 28 }), tokenData: (pack [(mkQuotient 65),(mkQuotient 58),(mkQuotient 72),(mkQuotient 101),(mkQuotient 108),(mkQuotient 108),(mkQuotient 111),(mkQuotient 32),(mkQuotient 87),(mkQuotient 111),(mkQuotient 114),(mkQuotient 108),(mkQuotient 100),(mkQuotient 33)]) })
[DEBUG] sent
[DEBUG] dry run result: (Right 0x)
[DEBUG] submitting to blockchain(DecodedMint { feeAmount: 1000000000000000000, nonce: 0, signature: (Signature { r: 0x195955fd323a05f96e2ef8c3ae29f811e5de87059379cf964304e7233b1b8998, s: 0x592a7cdecd027ee93440e2a6cec877ea4217db28e9e3fc365d73c9daea500492, v: 28 }), tokenData: (pack [(mkQuotient 65),(mkQuotient 58),(mkQuotient 72),(mkQuotient 101),(mkQuotient 108),(mkQuotient 108),(mkQuotient 111),(mkQuotient 32),(mkQuotient 87),(mkQuotient 111),(mkQuotient 114),(mkQuotient 108),(mkQuotient 100),(mkQuotient 33)]) })
[INFO] transaction successfully relayed with id 0xd9cc55aa569d36e30cdecddf1eb1b60a82621ccb80c132d58ef62a9c09a26990
```

**If this is so, our system is set up correctly.**
36 changes: 30 additions & 6 deletions dapp/DApp/Relay.purs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ module DApp.Relay
, transferRelayed'
, recoverRelayedMessageSignerWeb3
, recoverRelayedTransferSignerWeb3
, estimateMintRelayed
, estimateTransferRelayed
, callMintRelayed
, callTransferRelayed
) where

import DApp.Relay.Types
import Prelude (($))

import Contracts.RelayableNFT as RNFT
import DApp.Util (makeTxOpts, signABIFn', widenUIntN128, widenUIntN32)
import DApp.Util (estimateABIFn', callABIFn', makeTxOpts, signABIFn', widenUIntN128, widenUIntN32)
import Data.Either (Either)
import Network.Ethereum.Core.Signatures (PrivateKey)
import Network.Ethereum.Web3 (Address, CallError, ChainCursor, HexString, TransactionOptions, UIntN, Web3)
import Network.Ethereum.Web3 (Address, BigNumber, CallError, ChainCursor, HexString, TransactionOptions, UIntN, Web3)
import Network.Ethereum.Web3.Solidity.Sizes (S32)
import Network.Ethereum.Web3.Types (NoPay)
import Type.Proxy (Proxy(..))
Expand All @@ -27,11 +31,11 @@ getRelayNonce { nonceOf, checkFrom, checkAt, rnftAddress } =
in RNFT.getCurrentRelayNonce txOpts checkAt { addr: nonceOf }

recoverRelayedMessageSignerWeb3 :: SignedRelayedMessage -> TransactionOptions NoPay -> ChainCursor -> Web3 (Either CallError Address)
recoverRelayedMessageSignerWeb3 (SignedRelayedMessage s) txOpts cc =
recoverRelayedMessageSignerWeb3 (SignedRelayedMessage s) txOpts cc =
RNFT.recoverRelayedMessageSigner txOpts cc { signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenData: s.tokenData }

recoverRelayedTransferSignerWeb3 :: SignedRelayedTransfer -> TransactionOptions NoPay -> ChainCursor -> Web3 (Either CallError Address)
recoverRelayedTransferSignerWeb3 (SignedRelayedTransfer s) txOpts cc =
recoverRelayedTransferSignerWeb3 (SignedRelayedTransfer s) txOpts cc =
RNFT.recoverRelayedTransferSigner txOpts cc { signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }

mintRelayed :: SignedRelayedMessage -> TransactionOptions NoPay -> Web3 HexString
Expand All @@ -42,12 +46,32 @@ mintRelayed' :: PrivateKey -> SignedRelayedMessage -> TransactionOptions NoPay -
mintRelayed' pk (SignedRelayedMessage s) txOpts =
signABIFn' (Proxy :: Proxy RNFT.MintRelayedFn) pk txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenData: s.tokenData }

transferRelayed :: SignedRelayedTransfer -> TransactionOptions NoPay -> Web3 HexString
transferRelayed (SignedRelayedTransfer s) txOpts =
RNFT.transferRelayed txOpts { signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }

transferRelayed' :: PrivateKey -> SignedRelayedTransfer -> TransactionOptions NoPay -> Web3 HexString
transferRelayed' pk (SignedRelayedTransfer s) txOpts =
signABIFn' (Proxy :: Proxy RNFT.TransferRelayedFn) pk txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }

estimateMintRelayed :: Address -> SignedRelayedMessage -> TransactionOptions NoPay -> Web3 BigNumber
estimateMintRelayed relayerAddress (SignedRelayedMessage s) txOpts =
estimateABIFn' (Proxy :: Proxy RNFT.MintRelayedFn) relayerAddress txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenData: s.tokenData }

estimateTransferRelayed :: Address -> SignedRelayedTransfer -> TransactionOptions NoPay -> Web3 BigNumber
estimateTransferRelayed relayerAddress (SignedRelayedTransfer s) txOpts =
estimateABIFn' (Proxy :: Proxy RNFT.TransferRelayedFn) relayerAddress txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }

callMintRelayed :: Address -> SignedRelayedMessage -> TransactionOptions NoPay -> Web3 HexString
callMintRelayed relayerAddress (SignedRelayedMessage s) txOpts =
callABIFn' (Proxy :: Proxy RNFT.MintRelayedFn) relayerAddress txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenData: s.tokenData }

callTransferRelayed :: Address -> SignedRelayedTransfer -> TransactionOptions NoPay -> Web3 HexString
callTransferRelayed relayerAddress (SignedRelayedTransfer s) txOpts =
callABIFn' (Proxy :: Proxy RNFT.TransferRelayedFn) relayerAddress txOpts $
{ signature: packSignature s.signature, feeAmount: (widenUIntN128 s.feeAmount), nonce: s.nonce, tokenID: (widenUIntN32 s.tokenID), destination: s.destination }

0 comments on commit e4567ea

Please sign in to comment.