Would you like to present some essential data in chart form? Jeff explains how he used Run BASIC to present data using a chart display to an IP address.
Not all displays are equal. While we’ve left CRT monitors in the dust, some are still limited to lower resolutions like the 800 × 600 or even 640 × 480. So we should keep this in mind when trying to display data. In addition, you might wish to limit the area a graphic uses for other reasons. So being able to configure the size of the display area can be important. For this project, I’ll be using Run BASIC, a personal web server that offers the basic programming tools needed for web design. This month, I’ll explain how to use Run to present data using a chart display to an IP address.
Run BASIC comes in three versions: Windows, Linux, and OS X. It is available to your local network or, if appropriately hosted, the Internet. The programming interface is through the same IP, and once you’ve written and debugged an application, Run BASIC can be told to serve the application, instead of the programming environment.
Suppose you’ve got some data collected from some sensor and want to see it in some form other than a list from the text file that has been logged by your device. For this project I used a file that was created from a project presented in my March 2014 article, “A Low-Cost Connection to the IoT” (Circuit Cellar 284). I was interested in collecting information on the 220-V pump used for retrieving water from a well serving my neighborhood.
SIZED TO FIT
Let’s start out with a simple application that displays a button a user can click on to change the width and height of a displayed graphic. The program listing in Listing 1 has two routines,
GraphicSize. Execution begins at the top.
Show begins by clearing the screen and placing a button at the top of the page. The button has two parameters, a label and a routine to execute,
GraphicSize, when the button is clicked. I defined the label as a combination of text and variable values. It describes the current width and height of the graphic we will produce. The wait statement halts the execution from continuing on into the next routine. At this point nothing else happens until the user clicks on the button.
Listing 1 This first application uses two routines.
[Show]begins with a clear screen and adds a button with the label Graphic Size (x,y), where x and y are the graphic width and height. If the button is pressed,
[Graphic Size]is executed where the user is asked for a new width and height. No input checking is done for this simple app. [Show] cls ' start with a clean screen 'add a button which will call a branch label button #GraphicSize, "Graphic Size (";x;",";y;")", [GraphicSize] wait ' done for now [GraphicSize] input "New Graphic Width";var ' ask for a new graphic width x=var ' assign x the new width input "New Graphic Height";var ' ask for a new graphic height y=var' assign y the new height goto [Show]
The button click event redirects the execution to the
GraphicSize routine. Input statements are used to gather information from the user. In this case the user inputs new values for the graphic width and height. Following these inputs, execution is again redirected to
Show, and because of the
cls statement, the button is shown with new width and height dimensions (see Figure 1).
Now we can add the outline for the graphic that is sized to the dimensions shown on the button. This will act as a frame to our graphic. As you can see in Listing 2, this has been expanded in a number of areas. First, a
gosub [Graphicbox] has been added beneath the button. Note the textless print statements just above this
gosub. It is used to add a carriage return. This is needed to bring the cursor back to the left or else the
graphicbox will be drawn just to the right of the button.
LISTING 2 The
[Initialize]routine prior to the
[Show]allows a user to preset some variables. In this case, I’m presetting the
#Graphicbox width and height. This prevents the initial
#GraphicBoxsize from being invisible, as uninitialized these would be 0. The
[GraphicSize]routine has been broken down into two similar routines that do error checking on the entered data. Finally, the
[Graphicbox]routine draws a box and renders the drawn items. Note this is drawn just below the button. [Initialize] GraphicboxWidth = 640 ' initial graphic width GraphicboxHeight = 480 ' initial graphic height ‘ [Show] cls ' start with a clean screen button #ScreenSize, "Graphic Size (";GraphicboxWidth;",";GraphicboxHeight;")", [GraphicSize] 'add a button which will call a branch label print ' just a carriage return and linefeed gosub [Graphicbox] print ' just a carriage return and linefeed wait ' done for now ' [GraphicSize] ' [GetWidth] input "New Graphic Width";var ' ask for a new screen width if var=nul then var = GraphicboxWidth if var < 64 or var > 800 then print "The width must be between 64 and 800, try again" goto [GetWidth] end if GraphicboxWidth = var ' assign GraphicboxWidth the new width ' [GetHeight] input "New Graphic Height";var ' ask for a new screen height if var=nul then var = GraphicboxHeight if var < 64 or var > 600 then print "The height must be between 64 and 600, try again" goto [GetHeight] end if GraphicboxHeight = var' assign GraphicboxHeight the new height ' goto [Show] ' [Graphicbox] graphic #graphicbox, GraphicboxWidth, GraphicboxHeight #graphicbox place(0, 0) #graphicbox box(GraphicboxWidth-1, GraphicboxHeight-1) render #graphicbox return
Graphicbox routine begins by defining a graphic area equal to the size of the requested width and height. At this point it is merely an empty space, so we begin by setting the
#Graphicbox object’s cursor coordinate to 0,0, the upper-left corner of the object. We then add a box from that coordinate set to the width-1,height-1 coordinate set. Finally, we render these activities to make them visible in the
GraphicSize routine has been broken into two pieces. Some legal data checks have been added to catch input errors before they can cause problems. The input statement is looking for a numeric value. If nothing is entered, we don’t want the original value to change, so we check for
nil (nothing). Then, if any entered value is out of legal range, we print a message to ask the user to try again and branch back to the input statement. The first routine was added to allow the initial graphic size to be set to something other than 0,0, which now we’ve defined as illegal.
When this application is run you will see both the resizing button and a visible graphic box sized to whatever the current dimensions displayed on the button (see Figure 2). The wonderful part about the program development with Run BASIC is the ability to run the application at any time. Prior to the application executing, it is compiled and errors are pointed out. It’s extremely easy to go back and forth between the editor and executing the application. It’s a good idea to save the app before running it; that way, if there is an error in your logic, you won’t lose any changes you may have made if the execution hangs or bombs. And, since this is all accessed via your browser, you can work from any computer on your network.
LABELS, VALUES, & GRIDS
Now that you have an idea of how we can not only present graphics but also enable a user to change what they see. We can fill in the remaining static graphic elements that will make up the chart for displaying some data. These are shown in Figure 3. Notice above the graphic box new buttons have been added: Graph Labels, Font/Size, Max/Min values, Major and Minor divisions, and Color. There is a lot happening here, so let’s begin with Graph Labels.
The graphic has three text labels, title, vertical, and horizontal. The title, at the top, describes the graph. The vertical and horizontal text labels describe the information being presented on each axis. Each of these has a separate font style, point size, and color associated with it. They have default parameters, but can be preset to other values in the initialization routine. The Point Size parameter describes height of the text in vertical pixels. Each character has its own width however. You can determine the total width of a graphic text string by using the statement
stringwidth(). These parameters are needed to not only place the text correctly but also to adjust other parameters (spacing between entities). The vertical text is handled differently, as it must be treated as a number of individual characters placed individually, instead of one horizontal string (see Listing 3).
LISTING 3 The Vertical label is displayed as individual characters. Their horizontal position is based on a fixed distance from the left edge of the #graphicbox. Their vertical position is based on the height of the #graphicbox (bottom of the #graphicbox), minus the PointSize × the number of characters in the string/2 (half of the total vertical size of the characters), plus the character offset × PointSize/2 (character offset). [GraphVLabel] #graphicbox font(VerticalLabelFont$, VerticalLabelPointSize) #graphicbox color(VerticalLabelColor$) ' center text and position it 2 points to the right of the left side of the Graphicbox VerticalLabelHeight = VerticalLabelPointSize for x=0 to len(VerticalLabel$)-1 #graphicbox place(2,((GraphicboxHeight-(VerticalLabelHeight * len(VerticalLabel$)))/2) + (x * VerticalLabelHeight+2)) #graphicbox "\";mid$(VerticalLabel$,x+1,1) next x return
Before additional items can be drawn, we need to know the size of the actual chart within our graphic box. The size is dynamic, that is its size will be adjusted based on not only these text labels, but also the vertical and horizontal axis values used to define the extents of the graph. We start by defining the actual graph boundaries,
GraphTop offset is based on the top edge of the graphicbox (0) and the Title Point Size, plus a few spacing pixels. The
GraphBottom offset is based on the bottom edge of the graphicbox (vertical size), minus the Horizontal Point Size, minus the Horizontal Axis Point Size, plus a few spacing pixels. The
Graphleft offset is based on the left edge of the graphicbox (0), plus the
VerticalLabel stringwidth() (1 character), plus the
VerticalAxis stringwidth() (2 characters), plus a few spacing pixels. The
GraphRight offset is based on the right edge of the graphicbox (horizontal size), minus a few Horizontal Character widths (so the
HorizontalAxisMax has room for its label). We now have the coordinates for an inner box (the actual graph) with UL (top left) and BR (bottom right) coordinates maximized on various label and value sizes.
Each axis has values that define the graph display boundaries as well as major and minor axis divisions. The vertical axis maximum and minimum values are displayed at the graphtop and graphbottom, respectively. The vertical axis minor value determines how often a horizontal minor grid division is placed. A second value offers major grid division markers. In this case the vertical maximum is 10 and the minimum is 0. Minor grid spacing is 1, every integer between min and max. The major grid spacing is 5, every 5 integers. Note the axes are actually labeled at all major grids. When using both minor and major divisions, it is important to make these different colors or you will not be able to tell the difference between them. Since the objects in the #graphicbox are drawn in layers, it also makes a difference in which order the grids are drawn, here we draw the major grid on top of (after) the minor grid. The horizontal axis has identical parameters but are dependent on
GraphRight for positioning.
Collecting data is an art in itself. Make a mistake in how it’s collected and you can end up with useless information. Temperature data, for instance, comes in many forms. If we’re talking comfort in your home, you might be interested in temperatures between 60°F and 80°F. Your refrigerator on the other hand requires temperatures much lower or we have spoilage. When soldering with a sn60/pb40 alloy, you can’t even melt the solder until you reach the 361–374°F range. However, overheating can invisibly ruin parts. Time plays an important role in most situations.
While a call for heat (or cooling) from out thermostats is instantaneous, the unit cannot provide an immediate BTU exchange. The temperature cycle usually takes minutes. Because of the relatively low mass of a soldering iron tip, temperature changes can happen much faster, cycling in typically less than a second.
Often you are interested in knowing when a measurement is out of the normal operational range. This means your sampling equipment needs to be designed to operate within that range. Span and resolution are not only part of a sample value, but also sample rate. For temperature, we have a span or range of temperatures we are interested in as well as knowing the actual temperature to a resolution of a fraction of a degree. If we sampled that temperature once an hour we could very well miss an important event. Sampling once a second might be overkill if we know that the temperature cycle requires minutes.
While you might be interested in knowing when a temperature, current, color, weight, sound level, or other quantity exceeds a maximum or minimum amount. It is dangerous to assume the system is operating correctly if it will only send data during an alarm condition. A continuous stream of data (even within limits) is important in assuring that the equipment is in good working order.
When sample data includes date and time information along with data value(s), the data takes on a universal reference. It can be compared with other data of the same point in time no matter where it came from. For instance we might want to show how the temperature of our home relates to the ambient outside temperature for the same period.
For this project we’ll assume that the collected data is in prime condition. There are no missing or bad samples. To reduce the sample data, you may have decided to send only samples where the data changes are above some hysteresis level. This means the date/time of consecutive samples will not be periodic, but sporadic. To use this data you might have to fill in some gaps where the samples and time frame you want to display are nonexistent (haven’t changed in a while). This can be a pain and a good reason for not choosing this data logging shortcut.
To be able to display the data, we need to select parameters that make sense. For this discussion, I want to display some parameter over a period of time. I added the selection of a file to graph. You might want to display data from one particular file, in which case you can skip the selection process and hard code the file name into the application. The file will be read and the data placed into an array. When the data file is read, we can also pull out some max and min values like, “the first and last date and times, plus max and min values, as well as number of records.” With this info, we can automatically set up some graph parameters.
Prior to this point, I added buttons to allow you to set up some artificial parameters just to get a feel for how this is done. By extracting data from the file in question, we can have these set based on the actual data in the file. The two pieces of information I am using here are the maximum value of any data in the files. For this app I’ll assume the minimum is 0. Your maximum might be less than 255 (1 byte), 65,535 (2 bytes), or other multidigit value and you may wish to do some conversion on it to make it more relevant. For instance, if the range is 0-65,535 mA, you might wish to display this as 0–65 A. So a division by 1,000 would give a maximum of 65. This must be determined by you application, either fixed or user selectable.
My data is in this format. I know the current logged is in milliamps and will be less than 10 A, so the vertical minimum and maximum currents will be set to 0 and whatever the maximum value in the file is divided by 1,000. Horizontally, I want to show time. I chose to display some of the more relevant parameters to the user so that proper choices could be made, like choosing a date or time to display that is within the dataset. Assuming a good dataset (complete), the first two entries allow a calculation of the time resolution to determine if the data has been logged by the second, minute, or hour. This way the proper horizontal labels can be chosen.
The start date and start time of a graph is labeled on the buttons that allow a user to choose either. They are initialized to the first record in the files. The horizontal label describes the sample rate (weaned from the difference between the first two entries) and the major horizontal increment quantities. With all the graph framework finished, we cannot plot some data.
Since the start date and time were taken from the first entry in the file, we can begin to plot immediately. However, if the user changes the date and or time, we must scan the file entries until we match the request date and time. Without changes we begin plotting data with the first record from array,
MyRecord(index). The routine
RetrieveGraphData first checks to see that index is less than records, the number of records initially read from the file. A bad index returns with an
EndOfFile flag set so the graphing routine can exit with an alert.
Otherwise, the array string is pulled apart and each piece of information is assigned to a variable, so individual information can be extracted as necessary. The format for this string is:
<Date> <Time> <ID> <Channel> <Probe>
Each piece of data is separated by a <space>. Your data will most like be totally different, and you’ll need to alter the programming here to make your data accessible to the plotting function.
Once the first value has been returned it needs to be assigned an X and Y coordinate within the graph box’s inner box defined earlier by UL = (GraphLeft,GraphTop) and LR = (GraphRight,GraphBottom). We going to use every pixel we have available inside this box for the horizontal data. Y1 = GraphLeft. Vertically, a data value of 0 will equal the GraphBottom with a maximum data value approaching GraphTop. GraphHeight = GraphBottom– GraphTop, or the number of vertical pixels available. Span = VerticalAxisMax – VerticalAxisMin, or some number that represents the maximum data. The ratio of GraphHeight/Span gives us the vertical resolution of the graph, such that data value × the resolution is our offset from GraphBottom or X1 = GraphBottom – (value × GraphHeight/Span).
Now the next array value is extracted. Likewise, this value gets a second set of X and Y coordinates, X2 and Y2 (Y2 becomes the next horizontal pixel). These two records make the first dataset and their positions are plotted using the graphic function line (X1, Y1, X2, Y2). This draws a line between point X1,Y1 (the first data value) and point X2,Y2 (the second data value). By reassigning X1 = X2 and Y1 = Y2, we make room for the next record’s data value to be assigned to X2 and Y2. The next line segment can be drawn from the second data value to the third data value and so on until we’ve made our way to GraphRight (see Figure 4).
Once we’ve reached GraphRight the graph is rendered, a new button is added below the graph. This button gives the user the ability to proceed with a new graph of the next data. If this button is clicked, the selected date and time is refreshed with the last record read in the present graph, which presets the start of the next dataset.
NO CLOUDS HERE
I like the fact that Run BASIC can run on any PC and be contained to any device that has access to your local network. Or, if you want a system that is open to the Internet, you can have that too. I have an extra PC I use just as a server open to the internet through a free Dynamic DNS (Domain Name Server). Their service provides rerouting from a (chosen) hostname to the IP of your server, which most likely uses a dynamic IP address, via a small application that tells them your present IP address.
Today’s fast pace provides us with more ways of taking control or at least monitoring every aspect of our lives than ever before. While it takes a bit of programming to present an applications data, there is nothing like being able to view it from everywhere thanks to browsers. No wonder the IoT has become today’s buzz word. I see many manufacturers using the cloud, which allows them to offer all the bells and whistles without having to host a server and be responsible for all the headaches that go along with vertical and horizontal growth. Clouds do perform an important function, but sometimes I like to have complete control.
Run BASIC Personal server
Shoptalk Systems | www.runbasic.com
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JANUARY 2016 #306 – Get a PDF of the issue