Daniel TapaMatheus Bombig
Created August 11, 2019 © GPL3+

Open a Locker with IOTA

You can keep anything safe inside a locker and pay using IOTA based on usage time.

IntermediateFull instructions providedOver 1 day917

Things used in this project

Hardware components

Raspberry Pi 3 Model B+
Raspberry Pi 3 Model B+
×1
Raspberry Pi Touch Display
Raspberry Pi Touch Display
×1
Lock-style solenoid 12V
×1
Breadboard (generic)
Breadboard (generic)
×1
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Power MOSFET N-Channel
Power MOSFET N-Channel
×1
Jumper wires (generic)
Jumper wires (generic)
×1
5V power supply for raspberry
×1
12V power supply (generic)
×1

Software apps and online services

Raspbian
Raspberry Pi Raspbian
IOTA Tangle
IOTA Tangle
Trinity Wallet

Hand tools and fabrication machines

Wood storage box

Story

Read more

Schematics

IOTA Locker Schematics

https://www.circuito.io/app?components=9443,200000,276649,842876

Flow chart

Steps and display screens

Code

locker.js

JavaScript
var IOTA = require ("@iota/core")				// iota.js library
const converter = require('@iota/converter');	// iota utils
var Gpio = require('onoff').Gpio;				// raspberry GPIO library
var lock = new Gpio(26, 'out');
var http = require('http');
var fs = require('fs');

// set remote iota node
const iota = IOTA.composeAPI({
    provider: 'https://nodes.thetangle.org:443'
})

// set seed
var seed = 'YOURSEEDHERE999999999999999999999999999999999999999999999999999999999999999999999'

var cost = 60;			  // cost per minute in iotas
var minimumCheckIn = 60;  // cost to start (check-in)
var watchInterval = 5000; // 5 seconds interval to check the tangle

const AVAILABLE = 0;	  
const IN_USE = 1;		  

var status; 			// AVAILABLE or IN_USE
var index;				// index for generating new addresses from seed
var checkInAddress;		// address to start the process
var checkOutAddress;	// address to finish and open the lock
var userBundle;			// bundle of the checkin tx sent by the user
var userAddress;		// address from user to interact
var paid;				// sum of balances from checkIn + checkOut
var checkInTime;		// time of use
var balanceWatchLoop;	// endless loop that keeps track of certain address
var timerLoop;			// loop during use for update cost and display
var errorCount;

// cost calculation
var checkInBalance;
var totalCost;

// check conectivity with iota node
iota.getNodeInfo()
    .then(info => {
    	console.log(info)
    	index = 60
    	reset()
    })
    .catch(error => {
        console.log(`Request error getNodeInfo: ${error.message}`)
    })

// reset all values
function reset() {
	status = AVAILABLE
	checkInTime = undefined
	checkInBalance = 0
	checkInAddress = null;
	checkOutAddress = null;
	totalCost = 0
	remainingAmount = 0
	errorCount = 0;
	userAddress = '';
	clearInterval(balanceWatchLoop)
	setTimer(false)
	setNextAddress();
}

// set new unused address (balance = 0)
function setNextAddress(async) {

	var address = IOTA.generateAddress(seed,index, 2, true)
	index++
	console.log(`Verifying address with index ${index}`)

	iota.getBalances([address],100)
		.then(({ balances }) => {
		    console.log(`..balance: ${balances}`)
		    errorCount = 0;
		    
		    if (balances != 0) {
		    	// check next address
		    	setNextAddress()
		    } else {
		    	// address is good to use

		    	if (status == AVAILABLE) {
		    		// set check-in address
		    		checkInAddress = address
		    		console.log(`..checkInAddress set: ${address}`)
	    			startBalanceWatch()

		    	} else {
		    		// set check-out address
		    		checkOutAddress = address
		    		console.log(`..checkOutAddress set: ${checkOutAddress}`)
		    		sendCheckOutAddress()
		    		startBalanceWatch()
		    	}
		    }
		})
		.catch(err => {
			errorCount++;
			console.log(`Request error ${errorCount} getBalances: ${err.message}`)
			if (errorCount < 3) {
				setNextAddress()
			}
	    })
}

// keep checking income transactions
function startBalanceWatch() {

	console.log(`Starting balance watch...`)
	balanceWatchLoop = setInterval(function() { 
			
			if (status == AVAILABLE) console.log('Waiting check-in...');

			addressToWatch = status == AVAILABLE ? checkInAddress : checkOutAddress
			if (addressToWatch === null) return; // may happen if it takes longer to define next address

			iota.findTransactionObjects({ addresses: [addressToWatch] })
		   		.then(transactions => {
		   			var sum = 0;
		       		for (var i=0; i < transactions.length; i++) {
		       			sum += transactions[i].value;
		       		}

					if (status == AVAILABLE) {
						if (sum >= minimumCheckIn) {
							
							// check-in
							console.log(`***** CHECKING IN! *****`)

							clearInterval(balanceWatchLoop)
							status = IN_USE
							checkInBalance = sum;
							userBundle = transactions[transactions.length - 1].bundle;
							openLock()
							setTimer(true)
							setNextAddress()
						}
					} else {
						// calculate values
						paid = (parseInt(checkInBalance) + parseInt(sum))
						if (paid === undefined) paid = "";
						var remainingAmount = parseInt(totalCost) - paid;
						if (remainingAmount < 0) remainingAmount = 0;

						console.log(`(check-in ${checkInBalance}) + (check-out ${sum}) = Paid: ${paid} | Total cost: ${totalCost}`)

						if (totalCost > minimumCheckIn && paid >= totalCost) {

							// check-out
							console.log(`***** CHECKING OUT! *****`)

							openLock()								
							reset()
						}
					}

		   		})
		   		.catch(err => {
		       		console.log(`Request error findTransactionObjects: ${err.message}`)
		   		})

		}, watchInterval);
}

// open locker through gpio
function openLock() {

	console.log(`Opening lock...`);
	setInterval(function() {
		lock.writeSync(0);	
	}, 6000); // keep locker 6 seconds open
	lock.writeSync(1);
}

// usage timer
function setTimer(start) {

	if (start) {
		console.log(`Starting timer...`);

		// increase cost every minute
		timerLoop = setInterval(function() {
			totalCost += cost;	
		}, 60000);
		
	} else {
		clearInterval(timerLoop)
		console.log(`Timer stopped!`);
	}
}

// after check-in, send new unused (checl-out) address to user
function sendCheckOutAddress() {

	iota.findTransactionObjects({ bundles: [userBundle] })
	   		.then(transactions => {
				console.log('Transactions found: ' + transactions.length);
	       		for (var i=0; i < transactions.length; i++) {
	       			if (transactions[i].value < 0) {
	       				userAddress = transactions[i].address;
						break;
	       			}
	       		}

	       		console.log(`User address: ${userAddress}`);
	       		if (userAddress == '') {
	       			console.log(`Could not determine user address`)	
	       			return;
	       		}

				// send message to user
				const transfers = [{
				    address: userAddress,
				    value: 0,
				    tag: '',
				    message: converter.asciiToTrytes(checkOutAddress)
				}]

				return iota.prepareTransfers(seed, transfers)
			})
			.then(trytes => {
		        console.log('transfer prepared!')
		        return iota.sendTrytes(trytes, 3, 14) // trytes, depth, minWeightMagnitude
		    })
		    .then(bundle => {
		        console.log(`Published transaction with tail hash: ${bundle[0].hash}`)
		        console.log(`Bundle: ${bundle}`)
		    })
	   		.catch(err => {
	       		console.log(`Error sendCheckOutAddress: ${err.message}`)
	   		})
}

// setup server for front-end requests
var server = http.createServer(function(req, res) {

	if (req.url == '/') {
		res.writeHead(200, {'Content-type': 'text/html'});
		var myReadStream = fs.createReadStream(__dirname + '/index.html', 'utf8');
		myReadStream.pipe(res);

	} else if (req.url == '/r') {
		res.writeHead(200, {'Content-type': 'aaplication/json'});

		var obj = {
			status: status,
			checkInAddress: checkInAddress,
			minimumCheckIn: minimumCheckIn,
			paid: (paid == null ? "" : paid),
			totalCost: totalCost + cost // charge extra minute for network delay
		}
		res.end(JSON.stringify(obj));

	} else {
		res.end('nothing here...');
	}
});
server.listen(3000, '127.0.0.1');

index.html

HTML
<!DOCTYPE html>
<html>
<head>
	<style>
		body{background: dodgerblue; font-family: verdana; color: #fff; padding-left: 20px; padding-right:20px; cursor: none;}
		h1{font-size: 48px; text-transform: uppercase; letter-spacing: 2px; text-align: center; margin-top:20px;}
		h2{font-size: 28px; text-transform: uppercase; letter-spacing: 1px; text-align: center; margin-top:35px;}
		table{font-size: 28px; width:100%; text-align: center}
	</style>
	<title>IOTA Locker</title>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
	<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>

</head>
<body>
	<h1 id="title">IOTA Locker</h1>
	<h2 id="centralMsg">Initializing...</h2>
	<table>
		<tr>
			<td id="leftMsg" style="padding-left:10px"></td>
			<td id="canvas"></td>
		</tr>
	</table>

	<script>

		$(function(){
			r = setInterval(getData, 1000);
    	});

		var r;
		var lastStatus = 0;
		var currentLayout = 0;
    	var getData = function() {
				$.get("http://localhost:3000/r", function(data, status) {

					if (data.status == 0) { // AVAILABLE

						if (lastStatus == 1) {

							// check-out
							clearInterval(r);
							setTimeout(function(){
								r = setInterval(getData, 1000);
							},15000);

							refresh(4);

						} else {
							if (data.checkInAddress == null) {

								// getting check-in address
								refresh(0);

							} else {

								// waiting check-in
								refresh(1, data);
							}
						}
					} else {
						if (lastStatus == 0) {

							// check-in
							console.log("Check-in!");
							refresh(2);
							clearInterval(r);
							setTimeout(function(){
								r = setInterval(getData, 1000);
							},20000);

						} else {
							
							// waiting check-out
							console.log("Waiting check-out..");
							refresh(3, data);
						}
					}
					lastStatus = data.status;
	    		});
	        };

	    // refresh screen
	    function refresh(layout, data) {

	    	if (layout != 3 && currentLayout == layout) return;

    		switch (layout) {

    			case 0: // getting check-in address
					$('body').css("background","dodgerblue");
					$('#title').html("IOTA Locker");
					$('#title').show();
					$('#centralMsg').html("Wait...");
					$('#centralMsg').show();
					$('#leftMsg').html("");
					$('#canvas').html("");
    				break;

				case 1: // waiting check-in
					var json = {
			        	address: data.checkInAddress,
			        	amount: 60,
			        	message: "",
			        	tag: ""
			        }
					$('body').css("background","dodgerblue");
			        $('#title').hide();
					$('#centralMsg').html("");
					$('#centralMsg').hide();
					$('#leftMsg').html("<b>IOTA LOCKER</b><br/><br/>Start sending<br/><b>60 iotas</b><br/>from your Trinity!<br/><br/>--->");
					$('#canvas').html("");
					new QRCode("canvas").makeCode(JSON.stringify(json));
					break;

				case 2: // check-in!
					$('body').css("background","mediumseagreen");
					$('#title').html("Check-in!");
					$('#title').show();
					$('#centralMsg').html("Put your stuff inside the box and close the door!");
					$('#centralMsg').show();
					$('#leftMsg').html("");
					$('#canvas').html("");
					break;

				case 3: // waiting check-out
					var paid = data.paid;
					if (paid == undefined || paid == "") paid = 60;
					var remaining = (data.totalCost - paid);
					if (remaining < 0) remaining = 0;
					$('body').css("background","darkkhaki");
					$('#title').html("IN USE");
					$('#title').show();
					$('#centralMsg').html("<small>Paid: " + paid + " | Cost: " + (data.totalCost == undefined ? "" : data.totalCost) + "</small><br/><br/>Send <b><u>" + remaining + " iotas</u></b> to the address you received as MESSAGE in your Trinity to unlock.");
					$('#centralMsg').show();
					$('#leftMsg').html("");
					$('#canvas').html("");
					break;

				case 4: // check-out!
					$('body').css("background","mediumseagreen");
					$('#title').html("Check-out!");
					$('#title').show();
					$('#centralMsg').html("You can grab your stuff!<br/><br/>Have a nice day!");
					$('#centralMsg').show();
					$('#leftMsg').html("");
					$('#canvas').html("");
					break;
    		}

    		currentLayout = layout;
	    }

	</script>

</body>
</html>

Bitbucket

Credits

Daniel Tapa

Daniel Tapa

1 project • 2 followers
Engineer & Entrepreneur
Matheus Bombig

Matheus Bombig

0 projects • 1 follower
Engineer and Entrepreneur

Comments