Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Malleus Maleficarum
Published

Mesh Fleet Tracking with Blockchain & Custom Cryptocurrency

This project wants to work in a decentralized way storing each event or alert in an Ethereum blockchain.

AdvancedFull instructions providedOver 1 day4,441
Mesh Fleet Tracking with Blockchain & Custom Cryptocurrency

Things used in this project

Hardware components

Helium Element Access Point (Cellular)
Helium Element Access Point (Cellular)
×1
Helium Atom Xbee Module
Helium Atom Xbee Module
×1
Helium Atom Raspberry PI Adapter
×1
Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
×1
OLED Display I2C
This component is optional and would be used only to validate the OBD connection data such as OBD protocol.
×1
Adafruit Ultimate GPS Breakout
Adafruit Ultimate GPS Breakout
×1

Software apps and online services

Go ethereum client

Hand tools and fabrication machines

CNC
Soldering iron (generic)
Soldering iron (generic)
Cable

Story

Read more

Schematics

Custom circuit

This schematics show the required wiring to allow work with GPS and Atom

Code

ATToken Contract

C/C++
AT Token minimum contract
pragma solidity ^0.4.18;

contract MYToken {

	struct LocationData {
        int speed;
        string longitude;
        string latitude;
    }

	mapping (address => uint256) public balanceOf;
	mapping (address => mapping (address => uint256)) public allowance;
	mapping (address => LocationData[]) locationData;
	
	// balanceOf[address] = 5;
	string public standard = "MYToken v1.0";
	string public name;
	string public symbol;
	uint8 public decimals; 
	uint256 public totalSupply;
	event Transfer(address indexed from, address indexed to, uint256 value);
	event NewLocation(address indexed from, int speed, string longitude, string latitude);


	function MYToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) public {
		balanceOf[msg.sender] = initialSupply;
		totalSupply = initialSupply;
		decimals = decimalUnits;
		symbol = tokenSymbol;
		name = tokenName;
	}

	function transfer(address _to, uint256 _value) public {
		require(balanceOf[msg.sender] > _value);
		require(balanceOf[_to] + _value > balanceOf[_to]);

		balanceOf[msg.sender] -= _value;
		balanceOf[_to] += _value;
		emit Transfer(msg.sender, _to, _value);
	}

	function addLocation(address _address, int _speed, string _latitude, string _longitude) public {
        locationData[_address].push(LocationData(_speed, _longitude, _latitude));
		emit NewLocation(_address, _speed, _longitude, _latitude);
    }

}

Google IoT Core script

SH
This script allows you to add or remove the device registry in Google IoT Core.
#!/bin/bash

ACTION=$1
REGISTRY="my-registry"
PROJECT="google-project-id"
REGION="us-central1"

echo "$REGISTRY $PROJECT $ACTION"

if [ "$ACTION" = "delete" ]
then
    echo "Deleting registry $REGISTRY"

    gcloud iot devices list --registry=$REGISTRY --region="$REGION" | grep -v NUM_ID | while read id num
    do 
        gcloud iot devices delete --quiet "$id" --region="$REGION" --registry="$REGISTRY"
    done

    gcloud iot registries delete --quiet "$REGISTRY" --region="$REGION"
else
    echo "Creating registry $REGISTRY for project $PROJECT"
    gcloud iot registries create $REGISTRY --project=$PROJECT --region="$REGION" --event-notification-config=topic=projects/$PROJECT/topics/atom
fi

Python OBD scanner

Python
The scanner script
import obd

import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from helium_client import Helium

RST = None

disp = Adafruit_SSD1306.SSD1306_128_32(rst=RST)
width = disp.width
height = disp.height
image = Image.new('1', (width, height))
font = ImageFont.load_default()

draw = ImageDraw.Draw(image)


helium = Helium("/dev/serial0")
helium.connect()

channel = helium.create_channel("tracker")

disp.begin()
disp.clear()
disp.display()

draw.rectangle((0,0,width,height), outline=0, fill=0)

connection = obd.OBD() # auto-connects to USB or RF port

print "Available connections %s" % obd.scan_serial()

print "System connected : %s" % connection.status()

print "Procotol name %s" % connection.protocol_name();

draw.text((0, 0),"- %s" % connection.status(),  font=font, fill=255)
draw.text((0, 10),"- %s" % connection.protocol_name()[:12],  font=font, fill=255)
draw.text((0, 20),"- %s" % connection.protocol_name()[12:],  font=font, fill=255)

disp.image(image)
disp.display()

channel.send("System connected : %s" % connection.status())
channel.send("Procotol name %s" % connection.protocol_name())

print connection.query(obd.commands.RPM)

ConnectedCar

Python
The connected car python script that sends data to Helium
NOTE : This script is not fully tested yet.
import obd
import time
import json
from helium_client import Helium
from neo6 import GpsNeo6
import RPi.GPIO as gpio
import os

ATOM_CTRL_GPIO = 23
GPS_CTRL_GPIO = 24

helium = None
gps = None
channel = None
connection = obd.OBD()
file = None

alreadySync = False

gpio.setmode(gpio.BCM)# or gpio.setmode(gpio.BOARD)
gpio.setup(ATOM_CTRL_GPIO, gpio.OUT)
gpio.setup(GPS_CTRL_GPIO, gpio.OUT)

print "Available connections %s" % obd.scan_serial()
print "System connected : %s" % connection.status()
print "Procotol name %s" % connection.protocol_name();

while True:
    rpms = connection.query(obd.commands.RPM)
    coolantTemp = connection.query(obd.commands.COOLANT_TEMP)
    speed = connection.query(obd.commands.SPEED)
    runtime = connection.query(obd.commands.RUN_TIME)
    fuelLevel = connection.query(obd.commands.FUEL_LEVEL)
    
    data = {}
    data["s"] = speed
    data["v"] = "01"#The current device Id

    if(rpms > 0):
        alreadySync = False
        #If the car is turned on ... dump the data to the file, send HIGH to the GPS GPIO port, read the GPS data; also release the serial port
        if(helium.connected()):
            helium.close()
            del channel
            del helium
        
        if(file is None):
            file = open("/tmp/obd.json","a") 
        
        gpio.output(GPS_CTRL_GPIO, GPIO.HIGH)
        gpio.output(ATOM_CTRL_GPIO, GPIO.LOW)

        if gps is None:
            gps = GpsNeo6(port="/dev/serial0", debit=9600, diff=2)

        gps.traite()      

        data["la"] = gps.latitude
        data["lo"] = gps.longitude

        file.write(json.dumps(data) + "\n");
    else:
        del gps

        gpio.output(GPS_CTRL_GPIO, GPIO.LOW)
        gpio.output(ATOM_CTRL_GPIO, GPIO.HIGH)

        if(helium is None):
            helium = Helium("/dev/serial0")

        if(!helium.connected()):
            helium.connect()
            channel = helium.create_channel("Google IoT Core")

        #If is not synced ... read the file and send it to Helium
        if(!alreadySync):
            alreadySync = True

            line = file.readline()
            while line:
                channel.send(line)

                line = file.readline()
            
            file.close()
            del file

            os.remove("/tmp/obd.json")

    #lets sleep for 10 seconds after asking again for the data
    time.sleep(10)

gpio.cleanup()

webapp/lib/http.js

JavaScript
The http server
"use strict";

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const bunyan = require('bunyan');
const exphbs  = require('express-handlebars');
const expressWs = require('express-ws')(app);
const argv = require('minimist')(process.argv.slice(2));
const path = require('path');

var wsEndpoint = expressWs.getWss('/ws');
var config;
var log;
var ethereum;
var routesCache = [];
var contract;

//TODO This has to be get when login
const vehicleAddress = "0xc0d69b9c0cb475c01c94f924946149a644e593db";

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

var modules = module.exports = {
  configure: (_config_, _eth_) => {
    config = _config_;
    ethereum = _eth_;

    log = modules.createLogger("webserver");

    app.engine('handlebars', exphbs({
      defaultLayout: 'main',
      layoutsDir: path.join(__dirname, 'views/layouts')
    }));

    app.set('view engine', 'handlebars');
    app.use(express.static(path.join(__dirname, 'public')));
    app.set('views', path.join(__dirname, 'views'));
    app.use('/dh7', express.static(__dirname + '/public'));



    ethereum.startWatching((event) => {
      console.log("Trying to push to websockets ", event);

      wsEndpoint.clients.forEach(function(client, idx) {
        client.send(JSON.stringify({"event":event}));
      });
    });

  },
  start:() => {
    app.listen(config.port, function () {
      log.info('Listening on port ' , config.port);
    });
  },
  createLogger: (module) => {
    return bunyan.createLogger({
      name: module,
      src: false,
      streams: [
        {
          level:'debug',
          stream: process.stdout
        }
      ]
    });
  }
};


app.get('/', (req, res) => {
  res.render('index', {layout:false, account: {"address":vehicleAddress, "balance": ethereum.getAccountBalance(vehicleAddress)}});
});

app.post('/events', (req, res) => {
  console.log(req.body);
  const body = new Buffer(req.body.base64, 'base64').toString();
  const json = JSON.parse(body);

  ethereum.addLocation(config.ethereum.defaultAddress, json.s, json.la, json.lo, (error, value) => {
    const tx = ethereum.getWeb3().eth.getTransaction(value);
    log.info("Transaction received from blockchain ", tx);
  });

  res.status(200).json({"message":"Event processed"});
});

app.ws('/ws', function(ws, req) {
  ws.send(JSON.stringify({"contest":"helium"}));
});

webapp/lib/ethereum.js

JavaScript
The ethereum client
const Web3 = require('web3');
const web3 = new Web3();

var config;
var contract;
var log = console;

const self = module.exports = {
    configure: (c) => {
        config = c;

        web3.setProvider(new web3.providers.HttpProvider(config.ethereum.provider));
        web3.eth.defaultAccount= config.ethereum.defaultAddress;

        contract = web3.eth.contract(config.ethereum.abi).at(config.ethereum.contractAddress);

        log.info("Accounts ", web3.eth.accounts);

        web3.eth.accounts.forEach(address => {
            const balance = web3.eth.getBalance(address);
            log.info(address + " balance " + balance.c[0]);
        });

    },
    transfer: (to, amount, callback) => {
        contract.transfer.sendTransaction(to, amount, callback(error, value));
    },
    addLocation: (src, speed, lat, lon, callback) => {
        contract.addLocation.sendTransaction(src, speed, lat, lon, function(error, value) {
            callback(error, value);
        });
    },
    getAccountBalance: (address) => {
        return web3.eth.getBalance(address);
    },
    getWeb3: () => {
        return web3;
    },
    startWatching: (callback) => {
        contract.NewLocation().watch(function(error, result){
            if (!error) {
                log.debug("New location ", result);
                callback(result);
            } else {
                log.error("Error processing tranction ", error);
            }
        });

        contract.Transfer().watch(function(error, result){
            if (!error) {
                log.debug("Transfer ", result);
                callback(result);
            } else {
                log.error("Error processing tranction ", error);
            }
        });
    }
};

webapp/index.js

JavaScript
The entry point of the server
const ethereum = require('./lib/ethereum.js');
const http = require('./lib/http.js');
const config = require('./config/config.json');

ethereum.configure(config);
http.configure(config, ethereum);

http.start();

webapp/config/config.json

JSON
The config file for the server
{
    "ethereum": {
        "provider":"http://localhost:8545",
        "defaultAddress":"0x34b35496E9654B510A0B4d4760A541546a08062C",
        "contractAddress":"0xf792956A36c7448f3F2D7AfEa2f33EA312f2B333",
        "abi":[ { "constant": true, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string", "value": "MYT" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256", "value": "1000" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8", "value": "2" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "standard", "outputs": [ { "name": "", "type": "string", "value": "MYToken v1.0" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "", "type": "uint256", "value": "0" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_address", "type": "address" }, { "name": "_speed", "type": "int256" }, { "name": "_latitude", "type": "string" }, { "name": "_longitude", "type": "string" } ], "name": "addLocation", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string", "value": "MYT" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "address" }, { "name": "", "type": "address" } ], "name": "allowance", "outputs": [ { "name": "", "type": "uint256", "value": "0" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "inputs": [ { "name": "initialSupply", "type": "uint256", "index": 0, "typeShort": "uint", "bits": "256", "displayName": "initial Supply", "template": "elements_input_uint", "value": "1000" }, { "name": "tokenName", "type": "string", "index": 1, "typeShort": "string", "bits": "", "displayName": "token Name", "template": "elements_input_string", "value": "MYT" }, { "name": "tokenSymbol", "type": "string", "index": 2, "typeShort": "string", "bits": "", "displayName": "token Symbol", "template": "elements_input_string", "value": "MYT" }, { "name": "decimalUnits", "type": "uint8", "index": 3, "typeShort": "uint", "bits": "8", "displayName": "decimal Units", "template": "elements_input_uint", "value": "2" } ], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": false, "name": "speed", "type": "int256" }, { "indexed": false, "name": "longitude", "type": "string" }, { "indexed": false, "name": "latitude", "type": "string" } ], "name": "NewLocation", "type": "event" } ]
    },
    "port":3001
}

webapp/lib/public/js/app.js

JavaScript
app.js to handle webclient events
var APP = {
    init: (eventCallback) => {
        map = L.map('mapid').setView([37.785171, -122.397198], 16);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.hackster.io/maleficarum" target="_blank"> Malleus Maleficarum </a>'
        }).addTo(map);   
            
        if(!("WebSocket" in window)){
                alert("Websockets not supported");
        } else {
            socket = new WebSocket("ws://localhost:3001/ws");
        
            socket.onopen = function(){
                console.log("Connected to websocket server");
            }
        
            socket.onerror = function() {
                console.error("Error connecting to websocket server");
                setTimeout(function() {
                  location.reload();
                }, 60000);
            }
        
            socket.onmessage = function(msg) {
                const json = JSON.parse(msg.data);
        
                console.log("Message received by WS ", json);
                if(json.alert) {
                    self.notifications.send(json.alert);
                } else {
                    eventCallback(JSON.parse(msg.data));
                }
            }
        }            
    }
};

webapp/lib/views/index.handlebars

JavaScript
The main page
<html>
    <head>
        <title>AssetTracking</title>
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin=""/>        
        <link rel="stylesheet" href="https://getbootstrap.com/docs/4.1/examples/sticky-footer/sticky-footer.css" />
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" />
        <script src="/js/app.js" ></script>
        <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js" integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw==" crossorigin=""></script>
        <script src="https://code.jquery.com/jquery-3.3.1.min.js" ></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
        <style>
            #mapid { height: 500px; }
        </style>
    </head>
    <body>

    <!-- Begin page content -->
    <main role="main" class="container">
        <div align="center">
            <h1>Decentralized AssetTracker</h1>
            <div id="mapid"></div>

            <div class="card mb-4 box-shadow">
                <div class="card-header">
                    <h4 class="my-0 font-weight-normal">Current vehicle ( {{account.address}})</h4>
                </div>
                <div class="card-body">
                    <h1 class="card-title pricing-card-title">MT {{account.balance}} <small class="text-muted"></small></h1>
                    <ul class="list-unstyled mt-3 mb-4">

                    </ul>
                    <button type="button" class="btn btn-lg btn-block btn-outline-primary">Transfer MT coins</button>
                </div>
            </div>            
        </div>
    </main>

    <footer class="footer">
      <div class="container">
        <span class="text-muted">.</span>
      </div>
    </footer>
  </body>

     <script>
        var map;
        var assets = [];

        $(document).ready(() => {
            APP.init(((message) => {
                const event = message.event;
                if(!event) {
                    return;
                }
                console.log("Message received ", event);

                if(event.event === "Transfer") {
                    location.reload();
                }
                var marker;

                //Check if the asset already is in the map
                assets.forEach(pin => {
                    if(pin) {
                        marker = pin;
                    }
                });

                if(marker) {
                    marker.marker.setLatLng([event.args.latitude, event.args.longitude]);
                } else {
                    var m = L.marker([event.args.latitude, event.args.longitude]);
                    m.addTo(map)
                        .bindPopup(event.address)
                        .openPopup();

                    const marker = {
                        "address":event.address,
                        "marker":m
                    }

                    assets.push(marker);                    
                }

                console.info(assets);
            }));
        });
    </script>
</html>

Credits

Malleus Maleficarum

Malleus Maleficarum

5 projects • 28 followers
I'm a maker and a sorftware architect that loves to connect the real world to the internet.

Comments