Tlönizer

Photo on 12-16-15 at 20.33

Physical Input:

I used an Ardunio MEGA ADK and an Adafruit PN532 RFID/NFC Reader to read RFID stickers on pieces of paper. I also have a rotary encoder connected to the Ardunio to measure the movement of the conveyer belt.

Here’s a diagram of how my project looks like:

pngfinal

Here is a photo of how the PN532 looks like over the arduino:

Photo on 12-16-15 at 20.10Photo on 12-16-15 at 20.10 #2

And of how it looks inside of its container:

Photo on 12-16-15 at 20.11

And a photo of the rotary encoder:

Photo on 12-16-15 at 20.12

The code:

From the Arduino I receive the numbers of the objects/cards that are close to RFID reader. To do this I used the following code, kindly provided by Drool.

#include <Wire.h>
#include <Adafruit_PN532.h>

// If using the breakout or shield with I2C, define just the pins connected
// to the IRQ and reset lines.  Use the values below (2, 3) for the shield!
#define PN532_IRQ   (2)
#define PN532_RESET (3)  // Not connected by default on the NFC Shield

// Use this line for a breakout or shield with an I2C connection:
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);

long lastID = 0;

void setup() {
  Serial.begin(9600);

  nfc.begin();

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (! versiondata) {
    Serial.print("Didn't find PN53x board");
    while (1); // halt
  }

  // configure board to read RFID tags
  nfc.SAMConfig();

}


void loop() {
  int thisID;
  int success;
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };  // Buffer to store the returned UID
  uint8_t uidLength;                        // Length of the UID (4 or 7 bytes depending on ISO14443A card type)

  // Wait for an ISO14443A type cards (Mifare, etc.).  When one is found
  // 'uid' will be populated with the UID, and uidLength will indicate
  // if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight)
  success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength);

  if (success) {
    // Display some basic information about the card
    if (uidLength == 4)
    {
      // We probably have a Mifare Classic card ...
      unsigned long cardid = uid[0];
      cardid <<= 8;
      cardid |= uid[1];
      cardid <<= 8;
      cardid |= uid[2];
      cardid <<= 8;
      cardid |= uid[3];
      thisID = cardid;
    //  if (thisID != lastID) {
        Serial.println(cardid); // print the card number
     // }
      lastID = thisID;
    }
  }
}

Here’s a screenshot of the console logging all the numbers of the cards:

Screen Shot 2015-12-16 at 21.09.23

I couldn’t have both the RFID and encoder numbers, but that’s something I’ll work on the following days.

Digital Fabrication:

For the project I built a scanner with a conveyer belt:

Photo on 12-16-15 at 20.56

I designed the parts using tinkercad:

Screen Shot 2015-12-16 at 21.13.43 Screen Shot 2015-12-16 at 21.12.55

I laser cutter the parts and assembled them together here’s how the project looked:

Photo on 12-16-15 at 20.33

I used an LCD screen to display my p5.

Photo on 12-16-15 at 20.44Photo on 12-16-15 at 20.55 #2

The code: 

I decided to use javascript and p5.js for making the code. Here’s what it did:

  1. It received data from the arduino indicating the page that was being scanned
  2. It retrieved the content of the page from a cloudant database
  3. It NLPed the text to find the nouns
  4. It looked for the definitions of those nouns in the Merriam Webster dictionary
  5. It NLPed the definitions to find their nouns
  6. Using Regex it deleted the nouns in the definitions
  7. Using Regex it replaced the definitions without nouns for the nouns in the text
  8. It displayed the text using p5.js

app.js:

var express = require('express');
var Request = require('request');
var bodyParser = require('body-parser');
var WebSocketServer = require('ws').Server;
var ardsdat;
var app = express();

// Set up the public directory to serve our Javascript file
app.use(express.static(__dirname + '/public'));
// Set EJS as templating language
app.set('views', __dirname + '/views');
app.engine('.html', require('ejs').__express);
app.set('view engine', 'html');
// Enable json body parsing of application/json
app.use(bodyParser.json());

/* ---------
DATABASE Configuration
----------*/
// The username you use to log in to cloudant.com
var CLOUDANT_USERNAME="lmn297";
// The name of your database
var CLOUDANT_DATABASE="tlontexts";
// These two are generated from your Cloudant dashboard of the above database.
var CLOUDANT_KEY="tiaegantiedideethedoings";
var CLOUDANT_PASSWORD="26991b75193f6bd5f2b10de1fe04d674eb8b0162";

var CLOUDANT_URL = "https://" + CLOUDANT_USERNAME + ".cloudant.com/" + CLOUDANT_DATABASE;


/* ----
ROUTES
-----*/
// GET - route to load the main page
app.get("/", function (request, response) {
	console.log("In main route");
	response.render('index', {title: "Tlon"});
});

// POST - route to create a new note.
app.post("/save", function (request, response) {
	console.log("Making a post!");
	// Use the Request lib to POST the data to the CouchDB on Cloudant
	Request.post({
		url: CLOUDANT_URL,
		auth: {
			user: CLOUDANT_KEY,
			pass: CLOUDANT_PASSWORD
		},
		json: true,
		body: request.body
	},
	function (err, res, body) {
		if (res.statusCode == 201){
			console.log('Doc was saved!');
			response.json(body);
		}
		else{
			console.log('Error: '+ res.statusCode);
			console.log(body);
		}
	});
});


// GET - API route to get the CouchDB data after page load.
app.get("/api/:key" , function (request, response) {
	var theNamespace = request.params.key;
	console.log('Making a db request for namespace ' + theNamespace);
	// Use the Request lib to GET the data in the CouchDB on Cloudant
	Request.get({
		url: CLOUDANT_URL+"/_all_docs?include_docs=true",
		auth: {
			user: CLOUDANT_KEY,
			pass: CLOUDANT_PASSWORD
		},
		json: true
	}, function (err, res, body){
		//Grab the rows
		var theData = body.rows;

		if (theData){
			// And then filter the results to match the desired key.
			var filteredData = theData.filter(function (d) {
				return d.doc.namespace == request.params.key;
			});
			// Now use Express to render the JSON.
			response.json(filteredData);
		}
		else{
			response.json({noData:true});
		}
	});
});


app.get("/api" , function (request, response) {
	response.send(ardsdat);
});
/*app.get("/api/dic/:currentWord", function(req, res){
	//CORS enable this route - http://enable-cors.org/server.html
	res.header('Access-Control-Allow-Origin', "*");
	var currentWord = req.params.word;
	var requestURL = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + currentWord + "?key=55885b4e-0310-4926-b14e-b623f498d6f4";
	Request(requestURL, function (error, response, body) {
		if (!error && response.statusCode == 200) {
			//console.log(body);
			var theData = JSON.parse(body);
			//console.log(theData);
			res.json(theData);
		}
	});
});*/

// GET - Route to load the view and client side javascript to display the notes.
app.get("/:key", function (request, response) {
	console.log("In key...");
	response.render('notes',{title: "Tlon", key: request.params.key});
});

// GET - Catch All route
app.get("*", function(request,response){
	response.send("Sorry, nothing to see here.");
});

app.listen(3000);
console.log("Port 3000 started");

/*-----------
//WEB SOCKET
------------*/
/*
var SERVER_PORT = 8081;               // port number for the webSocket server
var wss = new WebSocketServer({port: SERVER_PORT}); // the webSocket server
//var connections = new Array;// list of connections to the server
var connections = [];

wss.on('connection', handleConnection);
 
function handleConnection(client) {
 console.log("New Connection"); // you have a new client
 connections.push(client); // add this client to the connections array
 
 client.on('message', sendToSerial); // when a client sends a message,
 
 client.on('close', function() { // when a client closes its connection
 console.log("connection closed"); // print it out
 var position = connections.indexOf(client); // get the client's position in the array
 connections.splice(position, 1); // and delete it from the array
 });
}

// This function broadcasts messages to all webSocket clients
function broadcast(data) {
	//var test = "Please work...";
 for (var i = 0; i < connections.length; i++) {   // iterate over the array of connections

  connections[i].send(data); // send the data to each connection
 }
}


/*-----------
//ARDS TALKS TO CONSOLE
------------*/

var serialport = require('serialport');// include the library
SerialPort = serialport.SerialPort; // make a local instance of it
// get port name from the command line:
portName = process.argv[2];

var myPort = new SerialPort(portName, {
	baudRate: 9600,
	// look for return and newline at the end of each data packet:
	parser: serialport.parsers.readline("\n")
	});

myPort.on('open', showPortOpen);
myPort.on('data', sendSerialData);
myPort.on('close', showPortClose);
myPort.on('error', showError);

function showPortOpen() {
   console.log('port open. Data rate: ' + myPort.options.baudRate);
}
 
function sendSerialData(data) {
   console.log(data);
   saveLatestData(data);
   ardsdat = data;
}

function saveLatestData(data) {
   console.log(data);
   // if there are webSocket connections, send the serial data
   // to all of them:
   // if (connections.length > 0) {
   //   broadcast(data);
   // }
}
 
function showPortClose() {
   console.log('port closed.');
}
 
function showError(error) {
   console.log('Serial port error: ' + error);
}

function sendToSerial(data) {
 console.log("sending to serial: " + data);
 myPort.write(data);
}


/*-----------
//TO CONSOLE START READING ARDS 
------------*/
//$ node app.js /dev/cu.usbmodem1421

Server side notepad:

var noteTemplate = function (data) {
	template = '<div class="note">';
	template += new Date(data.created_at);
	template += '<h3>'+ data.title +'</h3>';
	template += '<div>'+ data.text +'</div>';
	template += '</div>';

	return template;
};

// A function to accept an object and POST it to the server as JSON
function saveRecord (theData) {
	// Set the namespace for this note
	theData.namespace = window.key;
	console.log("Trying to Post");
	$.ajax({
		url: "/save",
		contentType: "application/json",
		type: "POST",
		data: JSON.stringify(theData),
		error: function (resp) {
			console.log(resp);
			// Add an error message before the new note form.
			$("#new-note").prepend("<p><strong>Something broke.</strong></p>");
		},
		success: function (resp) {
			console.log(resp);
			// Render the note
			var htmlString = noteTemplate(theData);
			$("#notes").append(htmlString);

			// Empty the form.
			$("#note-title").val("");
			$("#note-text").val("");
			// Deselect the submit button.
			$("#note-submit").blur();
		}
	});
}

// Loads all records from the Cloudant database. 
// Loops through them and appends each note onto the page.
function loadNotes() {
	$.ajax({
		url: "/api/"+window.key,
		type: "GET",
		data: JSON,
		error: function(resp){
			console.log(resp);
		},
		success: function (resp) {
			console.log(resp);
			$("#notes").empty();

			if (resp.noData){
				return;
			}

			// Use Underscore's sort method to sort our records by date.
			var sorted = _.sortBy(resp, function (row) { return row.doc.created_at;});

			// Now that the notes are sorted, add them to the page
			sorted.forEach(function (row) {
				var htmlString = noteTemplate(row.doc);
				$('#notes').append(htmlString);
			});
		}
	});
}

$(document).ready(function(){
	console.log("Loaded!");
	loadNotes();

	$("#new-note").submit(function () {
		// Get the information we want from the form including creating a new date.
		var noteData = {
			title: $("#note-title").val(),
			text: $("#note-text").val(),
			created_at: new Date()
		};

		//Send the data to our saveRecord function
		saveRecord(noteData);

		//Return false to prevent the form from submitting itself
		return false;
	});
});

Script.js

var numRequests;
var apiTermResponses = [];
var finaltextIsReady = false;
var loadtext = false;
var datards;
var thetlontextcat;
var thetlontext = '';
var theDT;
var finaltext = "Their language and those things derived from their language—religion, literature, metaphysics—presuppose idealism. For the people of Tlön,the world is not an amalgam of objects in space; it is a heterogeneous series of independent acts the world is successive, temporal, but not spatial. There are no nouns in the conjectural Ursprache of Tlön,from which its present-day languages and dialects derive: there are impersonal verbs, modified by mono-syllabic suffixes (or prefixes) functioning as adverbs. Please enter a page to look at how the language is transformed.";

var prevNumber = 0;

setInterval(readards, 1000);


function replacef(apiTermResponses){
	finaltext = thetlontext;
	for (var i = 0; i < apiTermResponses.length; i++){
		var curToBeRepl = apiTermResponses[i][0];
		var curBecome = apiTermResponses[i][1];
		finaltext = finaltext.replace(curToBeRepl, curBecome);
	}
	console.log('textreplaced');
	finaltext = finaltext.replace(/\s\s/g,' ');
	console.log(finaltext);
	finaltextIsReady = true;
}

function getAPIDic(term, orderVal){
	$.ajax({
		url: "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + term + "?key=21960e29-e95e-456c-af22-7ff4c8b2fedb",
		type: 'GET',
		dataType: 'xml',
		error: function(data){
				//console.log(data);
				alert("Oh No! Try a refresh?");
			},
		success: function(data){
			if (data.getElementsByTagName('dt')[0]){
				theDT = data.getElementsByTagName('dt')[0].textContent;
			}
			else{
				theDT = "";
			}

			var theNounArrayinDT = nlp.pos(theDT).nouns();
			var theTextArrayinDT = [];
			theNounArrayinDT.forEach(function(item){
			theTextArrayinDT.push(item.text);
			});

			for (var g = 0; g < theTextArrayinDT.length; g++){
				var curToBeRepl = theTextArrayinDT[g];
				theDT = theDT.replace(curToBeRepl, '');
			}

			for (var h = 0; h < replaceterms.length; h++){
				var curToBeRepla = replaceterms[h];
				theDT = theDT.replace(curToBeRepla, '');
			}

			var tempArray = [term, theDT];
			apiTermResponses.push(tempArray);
			if (apiTermResponses.length == numRequests){
				console.log('allhere');
				replacef(apiTermResponses);
				console.log(apiTermResponses);
				console.log('readytoreplaceterms');
			}

			else{
				console.log("Not yet...");
			}
		}
	});
}

function nalapo(thetlontext){
	var theNounArray = nlp.pos(thetlontext).nouns();
	var theTextArray = [];
		theNounArray.forEach(function(item){
		theTextArray.push(item.text);
	});
	console.log(theTextArray);
	loadtext = true;
	numRequests = theTextArray.length;
	console.log('allhere');
	theTextArray.forEach(function(item,i){
		var currentitem = item;
		for (var f = 0; f < replaceterms.length; f++){
			var curToBeRepl = replaceterms[f];
			currentitem = currentitem.replace(curToBeRepl, '');
		}
		console.log('nalapodone');
		//console.log(theNounArray);
		getAPIDic(currentitem,i);
	});
}

function getAPItexts(key){
	$.ajax({
		url: "/api/"+window.key,
		type: "GET",
		data: JSON,
		
		error: function(resp){
		},
		
		success: function (resp) {
			thetlontext = resp[0].doc.text;
			thetlontextcat = resp[0].doc.namespace;
			nalapo(thetlontext);
		},
	});
}

function readards(){
	$.ajax({
		url:"/api",
		type: "GET",
		data: JSON,

		error: function(resp){
		},
		
		success: function (resp){
			console.log(resp);
			datards = resp;
			console.log("Datards = " + datards);

			if (prevNumber != datards && prevNumber !== undefined){
				//Change!

				if(datards == 1910705701){
					card01();
				}else if( datards == 3251113509){
					card02();
				}else if(datards == 294456869){
					card03();
				}

			}

			prevNumber = datards;
		},
	});
}

card01 = function () {
		console.log('Hi, loading geography');
		resetGlobals();
		key = 'geography';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};

card02 = function () {
		console.log('Hi, loading history');
		resetGlobals();
		key = 'history';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};

card03 = function () {
		console.log('Hi, loading language');
		resetGlobals();
		key = 'language';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};

/*function pressfunc(){

	var btn = document.getElementById("geography");
	var btwn = document.getElementById("history");
	var btrn = document.getElementById("language");

	btn.onclick = function () {
		console.log('Hi, loading geography');
		resetGlobals();
		key = 'geography';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};

	btwn.onclick = function () {
		console.log('Hi, loading history');
		resetGlobals();
		key = 'history';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};

	btrn.onclick = function () {
		console.log('Hi, loading language');
		resetGlobals();
		key = 'language';
		getAPItexts(key);
		//thetlontext = '';
		//finaltext = '';
	};
}
*/

function resetGlobals(){
	apiTermResponses = [];
	//loadtext = false;
	//finaltextIsReady = false;
	thetlontext = 'Grabbing original text...';
	finaltext = 'Tlöning';
}

Sketch.js

var yoff = 0.0;
var keytag;
var word = "Of the fourteen names that figured in the section on geography, we recognized only three (Khorasan, Armenia, Erzerum), and they interpolated into the text ambiguously. Of the historical names, we recognized only one: the impostor-wizard Smerdis, and he was invoked, really, as a metaphor. The article seemed to define the borders of Uqbar, but its nebulous points of reference were rivers and craters and mountain chains of the region itself. We read, for example, that the Axadelta and the lowlands of Tsai Khaldun mark the southern boundary, and that wild horses breed on the islands of the delta.";

function preload(){
}

function setup() {
  createCanvas(windowWidth, windowHeight);

 
  // make a new div and position it at 10, 10:
  //console.log(text);
}

function draw() {
  background(51);
  fill(255);

  //Floating Graphic
  beginShape();
  var xoff = 0;

  for (var x = 0; x <= width; x += 10) {
    var y = map(noise(xoff, yoff), 0, 1, 150,250);
    vertex(x, y);
    xoff += 0.05;
  }

  yoff += 0.01;
  vertex(width, height);
  vertex(0, height);
  endShape(CLOSE);


  textFont("Helvetica");
  textSize(30);
  fill(0);
  text(finaltext, 110, 270, 1100, 350);
  //}

}

terms.js

var replaceterms = [
	/[:]/g,
	/[.]/g,
	/[,]/g,
	/[(]/g,
	/[)]/g,
	/\ba\b/g,
	/\band\b/g,
	/\bas\b/g,
	/\bof\b/g,
	/\bthe\b/g,
];

Html for posting:

<!DOCTYPE html>
<html>
	<head>
		<title><%= title %></title>
		<style>
			body {
				font-family: arial;
				max-width: 600px;
				min-width: 300px;
				margin: 1em auto;
			}
			input[type=text],
			textarea {
				display: block;
				width: 100%;
			}
			textarea {
				min-height: 150px;
			}
			.note {
				margin: 1em 0;
				padding: 1em 0;
				border-bottom: 1px solid gray;
			}
		</style>
		<script charset="utf-8" type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
		<script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script>
		<script type="text/javascript">
			window.key = '<%= key %>';
		</script>
		<script type="text/javascript" src="/js/serverside_notepad.js"></script>
	</head>
	<body>
		<h1><%= key %></h1>
		<form id="new-note">
			<input type="text" name="title" id="note-title" placeholder="Title"/>
			<textarea name="note" placeholder="note..." id="note-text"></textarea>
			<input type="submit" value="Save Note" id="note-submit"/>
		</form>
		<div id="notes">Loading...</div>
	</body>
</html>

Index.html

<!DOCTYPE html>
<html>

	<head>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"/></script>
		<link rel="stylesheet" type="text/css" href="style.css">
		<script src="https://rawgit.com/spencermountain/nlp_compromise/master/client_side/nlp.min.js"> </script>
		<script src="http://code.jquery.com/jquery-latest.js"/></script>
  		<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"/></script>
  		<script src="https://rawgit.com/spencermountain/nlp_compromise/master/client_side/nlp.min.js"/></script>
		<script type="text/javascript" src="/js/terms.js"></script>
  		<script type="text/javascript" src="/js/script.js"></script>
  		<script type="text/javascript" src="/js/sketch.js"></script>
  		<script type="text/javascript" src="/js/p5.min.js"></script>
		<script type="text/javascript" src="/js/p5.dom.js"></script>
  		<meta charset="utf-8">
		<title>Tlön</title>

	</head>
	<body>
		<div id="page-title">Tlönizer</div>
		<p></p>
	</body>
</html>

CSS:

html{
	font-family: Arial;
	font-size: 14px;
}

#outer{
    width: 100%;
    text-align: center;
    position: fixed;
    z-index: 9999;
    margin: 130px 1px 40px;
}

	.inner{
	    display: inline-block;
	}

		#geography{
		  	border-radius: 1px;
		  	border-style: solid;
		  	border-color: #ffffff;
		  	font-family: Arial;
		  	color: #4d4d4d;
		  	font-size: 21px;
		  	background: #ffffff;
		  	padding: 10px 20px 10px 20px;
		  	text-decoration: none;
		}

		#language{
		  	border-radius: 1px;
		  	border-style: solid;
		  	border-color: #ffffff;
		  	font-family: Arial;
		  	color: #4d4d4d;
		  	font-size: 21px;
		  	background: #ffffff;
		  	padding: 10px 20px 10px 20px;
		  	text-decoration: none;
		}

		#history{
		  	border-radius: 1px;
		  	border-color: #ffffff;
		  	border-style: solid;
		  	font-family: Arial;
		  	color: #4d4d4d;
		  	font-size: 21px;
		  	background: #ffffff;
		  	padding: 10px 20px 10px 20px;
		  	text-decoration: none;
		}

		#geography:hover {
  			background: #b3b3b3;
  			text-decoration: none;
		}

		#language:hover {
  			background: #b3b3b3;
  			text-decoration: none;
		}

		#history:hover {
  			background: #b3b3b3;
  			text-decoration: none;
		}

#page-title{
	font-size: 8em;
    text-align: center;
    margin: 10px 1px 40px;
    color: white;
    font-family: Arial;
    position: fixed;
    z-index: 9999;
    width: 100%;
}

How it looked at the end:

Photo on 12-16-15 at 20.44Photo on 12-16-15 at 20.42

People’s reactions:

General reactions were quite positive. Jill Maggi, Jim Savio, David Darts, and Johnny Farrow really liked the project and appreciated its connection to the short story by Borges.

There’s still a lot of work to do to make the processes more efficient and to avoid bugs! TTYL Scott 😀

 

Leave a Reply