Skip to content

Networking with many printers

Fons van der Plas edited this page Dec 5, 2018 · 3 revisions

One of the main features of the original printi was speed: images uploaded with the web site were printed instantly. The idea for printi mini was to allow anyone to have their own printer, which would print images sent to some printi.me/subdomain. Hosting a web site on every printi mini is not possible, because this requires the user to change their router settings to expose the printi mini to the internet (port forwarding). Therefore I decided to host a central web server, which would handle all file uploads, and send the images to the destination printer.

Crash course on IP routing

However, because of the way router firewalls work, it is not possible to send anything to a web connected device, even if you know their IP address.

This makes sense: there are often many devices connected to a single IP address. When I send a message to the printer's IP address, how would the router know which of the connected devices it was meant for? (If no port forwarding was set up)

On the other hand, web connected devices receive data all the time, send to them by the router. The difference is that routers only allow incoming data when the device requested the data beforehand from the sender. In technical terms: there needs to be an existing TCP/UDP connection (socket), recently initiated by the device. When you visit a web site, you establish a TCP connection with the IP address of that server. Because you initiated the connection, the server can now send you data (e.g. the HTML page) for the next couple of minutes, without the router blocking it.

printi

In the case of printi, this means that the central server cannot send anything to a printer, unless the printer recently requested it. So one way to make this work is to have every printer ask the central server for updates ("has anything been sent yet?") every x seconds. However,

  • if the time interval x is too large, it might take too very long before an image is printed (in the worst case, slightly longer than x seconds); 😴
  • if the time interval x is too small, the central server would get overloaded with requests, since every printi mini is constantly sending requests at a high rate. πŸ˜“

Solution

The way I solved this problem is to have the printer ask the server for updates at a low rate, about once every 60 seconds. However, if no image was uploaded, the server doesn't respond, and pretends to be thinking really hard. In reality, it's just sleeping for 60 seconds, waiting for anything to be uploaded during that time period from someone using the website. If something gets uploaded and processed, it wakes up immediately and responds with the processed image!

Since the printer requested the image data recently, it gets routed to the printer, and the image is printed.

Below is an implementation of this concept using Nancy. To run this yourself, get a sample project and replace the main module with the following:

public class Module : Nancy.NancyModule
{
	public static TaskCompletionSource<bool> tcs;
	public static string message;

	public Module()
	{
		Get["/send-something"] = _ =>
		{
			message = "πŸ”πŸ’•πŸŒ½";
			tcs?.TrySetResult(true);

			return "ok πŸ‘";
		};

		Get["/anything-sent", true] = async (_ctx, _ct) =>
		{
			// If this is not the first request for /anything-sent, stop the previous one
			tcs?.TrySetResult(false);
      
			// If a message has been set already, return it
			if(message != null)
			{
				return message;
			}
      
			// If not...
			// TaskCompletionSource is a Task (async thread) that will keep running (sleeping)
			// until its result is set from the outside.
			tcs = new TaskCompletionSource<bool>();
      
			// Wait for either tcs.Task or a 10 sec delay to finish.
			// If no request to /send-something is made during the next 10 seconds,
			// only the latter will finish.
			await Task.WhenAny(tcs.Task, Task.Delay(10*1000));
      
			// If the tcs was completed (instead of the Delay), and the message was set,
			// this means that a request to /send-something was made _while we were waiting_,
			// and the message should be returned.
			if(tcs.Task.IsCompleted && message != null)
			{
				return message;
			}

			// If not, the 10 sec delay must have run out.
			return "nothing was sent 😒";
		};
	}
}
Clone this wiki locally