📜 ⬆️ ⬇️

Redirecting data from the COM port to the Web

Recently on Habré there was an article "Displaying data from Serial in Chrome Application" about how beautifully to present the data sent by Arduin-koy to Serial. In my opinion, the guys offered a very beautiful solution, which on the one hand looks quite simple, and on the other allows you to get a great result with a minimum of effort.

In the comments to the article, it was regretted that such a solution would not work under Firefox, and the idea was expressed that "you can still write a simple web server with html output based on this thing." This idea “hooked” me, I didn’t give out a quick search in google, and I decided to implement the idea myself. And that's what came of it.

A warning! In no case can the proposed solution be regarded as complete. Unlike Amperka’s Serial Projector, this is a concept, a demonstration of a possible approach, a working prototype and nothing more.

Some time ago I did a project in which I used accelerometers built into the Android-smartphone to control the servers connected to the Arduino. Then for these purposes I used the Scripting Layer for Android (SL4A) and RemoteSensors projects. It turns out that the standard library python includes the BaseHTTPServer package, with which it is possible to raise a web service in python - this is a task for a couple of lines of code.
')
There were no sensors for the Arduino at hand, so I used the internal thermometer built into the Arduino Uno as the source of the displayed information. As far as I understand, it is not very accurate and is not intended to measure the ambient temperature at all, but it will completely come down for prototyping.

After a brief googling, this sketch for arduinka appeared:

// source: https://code.google.com/p/tinkerit/wiki/SecretThermometer long readTemp() { long result; // Read temperature sensor against 1.1V reference ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(MUX3); delay(2); // Wait for Vref to settle ADCSRA |= _BV(ADSC); // Convert while (bit_is_set(ADCSRA,ADSC)); result = ADCL; result |= ADCH<<8; result = (result - 125) * 1075; return result; } void setup() { Serial.begin(115200); } int count = 0; void loop() { String s = String(count++, DEC) + ": " + String( readTemp(), DEC ); Serial.println(s) delay(1000); } 

This sketch opens the COM port, adjusts it to the speed of 115200 baud, and then every second writes the current value of the built-in thermometer to it. (Do not ask me in what units the temperature is given - for the described task it is not important). Since the value does not change very actively, a line number is displayed before the temperature for better visibility of data changes.

To check that the web server will only send out whole lines, not parts of them, as they are read from the COM port,
  Serial.println(s) 

was replaced by
  for(int i=0; i < s.length(); i++ ){ Serial.print( s.charAt(i) ); delay( 200 ); } Serial.println(""); 

those. the generated string is not output to the serial port entirely, but character by character, with pauses of 200 ms.

To begin with, a very simple prototype of a web server was written (it is analyzed below in parts):
 # -*- coding: utf-8 -*- #-- based on: https://raw.githubusercontent.com/Jonty/RemoteSensors/master/remoteSensors.py SERIAL_PORT_NAME = 'COM6' SERIAL_PORT_SPEED = 115200 WEB_SERVER_PORT = 8000 import time, BaseHTTPServer, urlparse import serial ser = None def main(): global ser httpd = BaseHTTPServer.HTTPServer(("", WEB_SERVER_PORT), Handler) #-- workaround for getting IP address at which serving import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('google.co.uk', 80)) sData = s.getsockname() print "Serving at '%s:%s'" % (sData[0], WEB_SERVER_PORT) ser = serial.Serial(SERIAL_PORT_NAME, SERIAL_PORT_SPEED, timeout=0) httpd.serve_forever() class Handler(BaseHTTPServer.BaseHTTPRequestHandler): # Disable logging DNS lookups def address_string(self): return str(self.client_address[0]) def do_GET(self): self.send_response(200) self.send_header("Content-type", "application/x-javascript; charset=utf-8") self.end_headers() try: while True: new_serial_line = get_full_line_from_serial() if new_serial_line is not None: self.wfile.write(new_serial_line) self.wfile.write("\n") self.wfile.flush() except socket.error, e: print "Client disconnected.\n" captured = '' def get_full_line_from_serial(): """ returns full line from serial or None Uses global variables 'ser' and 'captured' """ global captured part = ser.readline() if part: captured += part parts = captured.split('\n', 1); if len(parts) == 2: captured = parts[1] return parts[0] return None if __name__ == '__main__': main() 

Let's sort the script in parts.

Since this is a prototype, all the basic operation parameters (the name of the COM port, its speed, and the number of the TCP port on which the web server will work) are indicated directly in the source text:
 SERIAL_PORT_NAME = 'COM6' SERIAL_PORT_SPEED = 115200 WEB_SERVER_PORT = 8000 

Of course, you can organize the reading of these parameters from the command line. For example, using the argparse module, this is done very quickly, simply and flexibly.

In this case, Windows users need to find out the name of the COM port to which Arduin is connected in Device Manager. I had it 'COM6'. Users of other OSes should use the tools of their OS. I have absolutely no experience with MacOS and in Linux I didn’t work with COM ports either, but most likely it will be something like "/ dev / ttySn".

Next comes the definition of a global variable to which an instance of the Serial class that is responsible for working with the COM port in python will be associated:
 ser = None 

In line
 httpd = BaseHTTPServer.HTTPServer(("", WEB_SERVER_PORT), Handler) 

a web server is created that will listen for requests on the specified WEB_SERVER_PORT port. And an instance of the Handler class, described below, will process these requests.

The following lines are a small “hack” that allows you to display the IP address on which the running web server actually runs:
  #-- workaround for getting IP address at which serving import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('google.co.uk', 80)) sData = s.getsockname() print "Serving at '%s:%s'" % (sData[0], WEB_SERVER_PORT) 

As I understand it, there is no other way to find out this IP. And how, without this knowledge, we will access our server from a browser?

Therefore, you have to open a socket and connect to the Google site in order to extract information about your own IP address from the attributes of this socket.

Just below the opening of the COM port and the actual launch of the web server:
  ser = serial.Serial(SERIAL_PORT_NAME, SERIAL_PORT_SPEED, timeout=0) httpd.serve_forever() 

Then follows the description of the class that is responsible for processing the requests received by the launched web server:
 class Handler(BaseHTTPServer.BaseHTTPRequestHandler): 

This is a descendant of the class built into the BaseHTTPServer module, in which it is enough to override only the do_GET method

Since this is still a prototype, the server will be “happy” to any request - whatever URL it is requested from, it will give the client all the data read from the COM port. Therefore, in Handler.do_GET, it immediately responds with a success code and the necessary headers:
  self.send_response(200) self.send_header("Content-type", "application/x-javascript; charset=utf-8") self.end_headers() 

after which an infinite loop starts, in which an attempt is made to read a whole line from the COM port and, if this attempt was successful, transfer it to the web client:
  while True: new_serial_line = get_full_line_from_serial() if new_serial_line is not None: self.wfile.write(new_serial_line) self.wfile.write("\n") self.wfile.flush() 

In the project, which was taken as a basis, this infinite cycle was wrapped in a try… except block, with which it was supposed to carefully handle the disconnection of the connection. Perhaps, in Android (the base project was developed for it), this works fine, but it didn’t work for me under Windows XP - when I disconnected, there was some other exception that I didn’t learn to intercept. But, fortunately, this did not prevent the web server from operating normally and accepting the following requests.

The function of receiving a whole string from a COM port works on the same principle as the creators of the Serial Projector:

 captured = '' def get_full_line_from_serial(): """ returns full line from serial or None Uses global variables 'ser' and 'captured' """ global captured part = ser.readline() if part: captured += part parts = captured.split('\n', 1); if len(parts) == 2: captured = parts[1] return parts[0] return None 

The result is:


It can be seen that the lines read from the COM port appear in the browser. I do not understand anything in the web front end: JavaScript, Ajax, CSS and DOM are for me a dark forest. But it seems to me that for programmers creating web interfaces this should be quite enough to convert this output into the same beautiful picture that Amperka’s Serial Projector produces. In my opinion, the task is to create a javascript script that accesses the web server, reads the stream from it and displays the last read line in the right place on the web page.

Just in case, I decided to play it safe and tried to make the first approach on my own. Not very deep search in Google prompted that generally for such purposes, at least, used WebSockets or Server-Sent Events earlier. I found, it seemed to me, a good tutorial on using Server-Sent Events and decided to use this technology.

Note! It seems that this is not the best solution, because this technology did not work either in Internet Explorer 8, or in the browser built into Android 2.3.5. But it worked at least in Firefox 39.0, so I did not “dig” further.

After reading this textbook, as well as another one in Russian , I took the simpl.info/eventsource project as a basis .

From the point of view of the Python script, the changes for Server-Sent Events are quite minor:


Everything else could probably remain unchanged, but ...

First, I created an index.html file with this content:
 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <h1></h1> <p id="data"></p> <script> var dataDiv = document.querySelector('#data'); var source = new EventSource('http://192.168.1.207:8000/') source.onmessage = function(e) { dataDiv.innerHTML = e.data; }; </script> </body> </html> 

The most interesting in it is a string
  <p id="data"></p> 

which forms a place to output the next line from the COM port, and a javascript script
  <script> var dataDiv = document.querySelector('#data'); var source = new EventSource('http://192.168.1.207:8000/') source.onmessage = function(e) { dataDiv.innerHTML = e.data; }; </script> 

which is actually engaged in reading the stream from the web server and outputting the read information to the specified location.

I assumed to open this file in a browser, for example, from a disk or from some other web server, but this did not work: when opening a page from disk, the javascript script once turned to the running Python web server and immediately disconnected. I did not understand why this was happening, and suggested that this might be some kind of browser protection against various attacks. He probably does not like the fact that the page itself is open from one source, and the script reads data from another source.

Therefore, it was decided to change the Python web server so that it gives this html page. Then it would happen that both the page and the stream are read from the same source. I do not know whether my assumption about security turned out to be true, or something else, but with this implementation everything worked as it should.

Of course, you only need to change the Handler class request handler:
 class Handler(BaseHTTPServer.BaseHTTPRequestHandler): # Disable logging DNS lookups def address_string(self): return str(self.client_address[0]) def do_GET(self): if self.path == '/' or self.path == '/index.html': self.process_index() elif self.path == '/get_serial': self.process_get_serial() else: self.process_unknown() def process_index(self): self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(open('index.html').read()) self.wfile.write("\n\n") self.wfile.flush() def process_get_serial(self): self.send_response(200) self.send_header("Content-type", "text/event-stream") self.end_headers() try: while True: new_serial_line = get_full_line_from_serial() if new_serial_line is not None: self.wfile.write('data: ' + new_serial_line) self.wfile.write("\n\n") self.wfile.flush() except socket.error, e: print "Client disconnected.\n" def process_unknown(self): self.send_response(404) 

In this variant, it is assumed that the web server will respond only to two requests: '/index.html' (giving the page's html-code) and '/ get_serial' (giving up an endless stream of lines read from the COM port). It will respond to all other requests with the code 404.

Since index.html is given to the Python web server, it can be slightly changed by specifying a relative address instead of the absolute address of the stream of lines from the COM port:
the string
  var source = new EventSource('http://192.168.1.207:8000/') 

replaced by
  var source = new EventSource('/get_serial') 

The result was this:



On this I decided to stop. It seems to me that it is beautiful to design a page - it should already be quite simple. But I do not own either HTML or CSS, so let someone else do it. I saw my task in showing that making a web service that provides data from a COM port is not at all difficult.

All sources can be taken on githabe .

I repeat: the presented code is not a complete solution that can be “let in production”. This is only a prototype that shows a fundamental approach to solving a problem.

What else can you work on:

In conclusion: the question may arise: “is it not easier to solve this problem through some ready-made cloud?”. Why not publish the data read from the COM port in the cloud, and on the clients simply access the corresponding service in the cloud? Probably, such a decision also has the right to exist, but before applying such a decision, you must answer the following questions:

Source: https://habr.com/ru/post/264663/


All Articles