Lawrence Technological University
College of Arts and Science
Department of Mathematics and Computer Sciences

Handouts

MCS 4982 Directed Studies, Embedded Linux Vaccine Storage Monitor

   The problem of ensuring that expensive vaccines are continously stored at acceptable temperatures is labor intensive and the data-loggers available now are somewhat expensive. The purpose of this course is two-fold:

  1. To study embedded linux and Python and Ruby
  2. To reduce both the labor and the expense by using the Raspberry Pi as a data logger and Web server

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 1: Setting up

   The initial setup and scaffolding:

Hardware
  1. Raspberry Pi with power supply and ethernet cable
  2. Waterproof DS18B20 sensor and 4.7 KΩ pullup resistor
  3. SD card for operating system and storage
  4. WiFi USB adapter (optional)
  5. Breakout board for GPIO pins on Raspberry Pi
  6. Green LED to indicate monitor on and in range and 1 KΩ current limiting resistor
Raspian Linux is loaded onto the SD card
# May 6, 2013:
# Start with 16GB SD Micro Class 10 -- probably larger than needed but
# the Class 10 speed allows loading the SD in about 1/3 the time.
pbook: $ cd Downloads
pbook:Downloads $ diskutil list/dev/disk0
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *1.0 TB     disk0
   1:                        EFI                         209.7 MB   disk0s1
   2:                  Apple_HFS Macintosh HD            999.9 GB   disk0s2
/dev/disk1
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *15.9 GB    disk1
   1:             Windows_FAT_32 NO NAME                 15.9 GB    disk1s1
pbook:Downloads $ diskutil unmountDisk
 /dev/disk1Unmount of all volumes on disk1 was successful
pbook:Downloads $ sudo dd bs=1m if=2013-02-09-wheezy-raspbian.img of=/dev/disk1
Password:
# Occasional Ctrl-T to check the progress for the impatient.
load: 0.81  cmd: dd 12332 uninterruptible 0.01u 1.40s
211+0 records in
210+0 records out
220200960 bytes transferred in 81.010646 secs (2718173 bytes/sec)
...
1850+0 records in
1850+0 records out
1939865600 bytes transferred in 779.510024 secs (2488570 bytes/sec)
pbook:Downloads $ sudo diskutil eject /dev/disk1
Password:
Disk /dev/disk1 ejected
Raspberry Pi booted and configured
pbook:~ jay$ ssh -l pi 192.168.1.70
pi@192.168.1.70's password:
# Entered raspberry. 
Linux raspberrypi 3.6.11+ #371 PREEMPT Thu Feb 7 16:31:35 GMT 2013 armv6l

NOTICE: the software on this Raspberry Pi has not been fully configured.
        Please run 'sudo raspi-config'

pi@raspberrypi ~ $ sudo raspi-config
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully

Current default time zone: 'US/Eastern'
Local time is now:      Sat May  4 20:39:47 EDT 2013.
Universal Time is now:  Sun May  5 00:39:47 UTC 2013.

# Resize the SD:
...
# Update:
raspi-config is already the newest version.
0 upgraded, 0 newly installed, 0 to remove and 157 not upgraded.
Sleeping 5 seconds before reloading raspi-config
# Restart:
pi@raspberrypi ~ $ sudo shutdown -r now
pbook:~ jay$ ssh -l pi 192.168.1.70
pi@192.168.1.70's password: 
pi@raspberrypi ~ $ df
Filesystem     1K-blocks    Used Available Use% Mounted on
rootfs          15251960 1622204  12991656  12% /
/dev/root       15251960 1622204  12991656  12% /
devtmpfs          216132       0    216132   0% /dev
tmpfs              44880     232     44648   1% /run
tmpfs               5120       0      5120   0% /run/lock
tmpfs              89740       0     89740   0% /run/shm
/dev/mmcblk0p1     57288   19000     38288  34% /boot
Emacs, Ruby and Screen are installed
# Emacs:
pi@raspberrypi ~ $ sudo apt-get install emacs
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  emacs23 emacs23-bin-common emacs23-common emacsen-common libfribidi0 libgpm2
  liblockfile-bin liblockfile1 libm17n-0 libotf0 m17n-contrib m17n-db
Suggested packages:
  emacs23-common-non-dfsg emacs23-el gpm m17n-docs gawk
The following NEW packages will be installed:
  emacs emacs23 emacs23-bin-common emacs23-common emacsen-common libfribidi0
  libgpm2 liblockfile-bin liblockfile1 libm17n-0 libotf0 m17n-contrib m17n-db
...
# Ruby:
$ sudo apt-get install ruby
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  libruby1.9.1 libyaml-0-2 ruby1.9.1
Suggested packages:
  ri ruby-dev ruby1.9.1-examples ri1.9.1 graphviz ruby1.9.1-dev ruby-switch
The following NEW packages will be installed:
  libruby1.9.1 libyaml-0-2 ruby ruby1.9.1
...
# Screen:
pi@raspberrypi ~ $ sudo apt-get install screen
Reading package list...
The optional Wifi adapter is configured
# New /etc/network/interfaces
pi@raspberrypi ~ $ cat /etc/network/interfaces
auto lo

iface lo inet loopback
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet dhcp
  wpa-ssid 2WIRE148
  wpa-psk 1234567890
# Removed with change of manual to dhcp 5-6-13
# wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp
pi@raspberrypi ~ $ ifconfig
eth0      Link encap:Ethernet  HWaddr b8:27:eb:f6:de:c2  
          inet addr:192.168.1.70  Bcast:192.168.1.255  Mask:255.255.255.0
...
wlan0     Link encap:Ethernet  HWaddr 00:e0:4c:10:40:7e  
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
...
pi@raspberrypi ~ $ sudo ifdown wlan0
pi@raspberrypi ~ $ sudo ifup wlan0
...
bound to 192.168.1.72 -- renewal in 42251 seconds.
pi@raspberrypi ~ $ logout
Connection to 192.168.1.70 closed.
pbook:~ jay$ ssh -l pi 192.168.1.72
pi@192.168.1.72's password: 
Linux raspberrypi 3.6.11+ #371 PREEMPT Thu Feb 7 16:31:35 GMT 2013 armv6l
The GPIO and 1-wire modules are configured to load at startup
pi@raspberrypi ~ $ ls /sys/bus/
amba         cpu           hid  mmc       scsi  spi
clocksource  event_source  i2c  platform  sdio  usb
pi@raspberrypi ~ $ sudo modprobe w1-gpio
pi@raspberrypi ~ $ sudo modprobe w1-therm
pi@raspberrypi ~ $ ls /sys/bus/
amba         cpu           hid  mmc       scsi  spi  w1
clocksource  event_source  i2c  platform  sdio  usb
pi@raspberrypi ~ $ cd /sys/bus/w1/devices/
pi@raspberrypi /sys/bus/w1/devices $ ls
28-000004473dc9  w1_bus_master1
pi@raspberrypi /sys/bus/w1/devices $ cd 28-000004473dc9
pi@raspberrypi /sys/bus/w1/devices/28-000004473dc9 $ ls
driver  id  name  power  subsystem  uevent  w1_slave
pi@raspberrypi /sys/bus/w1/devices/28-000004473dc9 $ cat w1_slave
34 01 4b 46 7f ff 0c 10 1c : crc=1c YES
34 01 4b 46 7f ff 0c 10 1c t=19250
# Before changes to /etc/modules:
pi@raspberrypi ~ $ lsmod
Module                  Size  Used by
snd_bcm2835            15846  0 
snd_pcm                77560  1 snd_bcm2835
snd_seq                53329  0 
snd_timer              19998  2 snd_pcm,snd_seq
snd_seq_device          6438  1 snd_seq
snd                    58447  5 snd_bcm2835,snd_timer,snd_pcm,snd_seq,snd_seq_device
snd_page_alloc          5145  1 snd_pcm
8192cu                489381  0 
leds_gpio               2235  0 
led_class               3562  1 leds_gpio
pi@raspberrypi ~ $ cat /etc/modules
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
# Parameters can be specified after the module name.

snd-bcm2835
w1-gpio
w1-therm
# Restart:
pi@raspberrypi ~ $ lsmod
Module                  Size  Used by
w1_therm                2987  0 
w1_gpio                 1308  0 
wire                   24629  2 w1_gpio,w1_therm
cn                      4794  1 wire
snd_bcm2835            15846  0 
snd_pcm                77560  1 snd_bcm2835
snd_seq                53329  0 
snd_timer              19998  2 snd_pcm,snd_seq
snd_seq_device          6438  1 snd_seq
snd                    58447  5 snd_bcm2835,snd_timer,snd_pcm,snd_seq,snd_seq_device
snd_page_alloc          5145  1 snd_pcm
8192cu                489381  0 
leds_gpio               2235  0 
led_class               3562  1 leds_gpio

pi@raspberrypi ~ $ cd /sys/bus/w1/devices/28-000004473dc9/
pi@raspberrypi /sys/bus/w1/devices/28-000004473dc9 $ cat w1_slave
36 01 4b 46 7f ff 0a 10 30 : crc=30 YES
36 01 4b 46 7f ff 0a 10 30 t=19375
# Interactive Ruby (like interactive Python) to test GPIO setup:
pi@raspberrypi /sys/bus/w1/devices/28-000004473dc9 $ irb
irb(main):001:0> File::readlines("w1_slave")
=> ["33 01 4b 46 7f ff 0d 10 08 : crc=08 YES\n", "33 01 4b 46 7f ff 0d 10 08 t=19187\n"]
irb(main):006:0> File::readlines("w1_slave").join.match(/YES\n.*t=(\d{4,6})/)[1] 
=> "18812"
# Exploring the enumeration of a directory -- same concept as .glob in Python. 
irb(main):001:0> ds = Dir["/sys/bus/w1/devices/28**/w1_slave"]
=> ["/sys/bus/w1/devices/28-000004473dc9/w1_slave"]
irb(main):002:0> ds
=> ["/sys/bus/w1/devices/28-000004473dc9/w1_slave"]
irb(main):003:0> ds.each {|f| open(f) {|w1| w1.each_line {|l| puts l}}}
49 01 4b 46 7f ff 07 10 f6 : crc=f6 YES
49 01 4b 46 7f ff 07 10 f6 t=20562
=> ["/sys/bus/w1/devices/28-000004473dc9/w1_slave"]
irb(main):004:0> ds.each {|f| open(f) {|w1| w1.each_line {|l| puts l}}}
49 01 4b 46 7f ff 07 10 f6 : crc=f6 YES
49 01 4b 46 7f ff 07 10 f6 t=20562
=> ["/sys/bus/w1/devices/28-000004473dc9/w1_slave"]
Access to GPIO#17 (LED) is enabled for user pi at startup
pi@raspberrypi ~ $ cat /etc/init.d/gpio17
#! /bin/sh
# /etc/init.d/gpio17
# Carry out specific functions when asked to by the system
case "$1" in
  start)
echo "17" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio17/direction
echo "0" > /sys/class/gpio/gpio17/value
chmod 666 /sys/class/gpio/gpio17/value
    ;;
  stop)
echo "0" > /sys/class/gpio/gpio17/value
echo "in" > /sys/class/gpio/gpio17/direction
echo "17" > /sys/class/gpio/unexport
    ;;
esac
exit 0
pi@raspberrypi ~ $ sudo -i
root@raspberrypi:~# chmod 755 /etc/init.d/gpio17
root@raspberrypi:~# update-rc.d gpio17 defaults 98 02
# Restart:
pi@raspberrypi ~ $ ls -l /sys/class/gpio/gpio17/value
-rw-rw-rw- 1 root root 4096 May 10 12:33 /sys/class/gpio/gpio17/value
pi@raspberrypi ~ $ cat /sys/class/gpio/gpio17/value
1
pi@raspberrypi ~ $ cat /sys/class/gpio/gpio17/direction
out
pi@raspberrypi ~ $ echo "0" > /sys/class/gpio/gpio17/value
pi@raspberrypi ~ $ cat /sys/class/gpio/gpio17/value
0
A ruby program to test the hardware arrangement
temptest2.rb is expanded a little to allow more practice with lower-level HTTP. For a database we just use a flat file. Our server will not run in a separate process or separate thread. The server will listen for INTERVAL seconds and then pause and take and record a temperature measurement.
#! /usr/bin/ruby
# temptest2.rb
require 'socket'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>temptest.html</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
# Constants that later may be stored on SD card and updated via the Web server:
HIGH_C = 22.0
LOW_C = 15.0
INTERVAL = 30

# Subroutines
def get_temp
  w1 = open(Dir["/sys/bus/w1/devices/28**/w1_slave"][0])
  if w1.gets =~ /YES/
    t = w1.gets.match(/t=(\d{4,6})/)[1].to_f * 0.001
  else 
    t = nil
  end
  w1.close
  # Adjust the Monitor On and In Range LED.
  green(t && t < HIGH_C && t > LOW_C)
  t
end

def log_entry
  ds = get_temp
  if ds
    le = "%5.1f" % ds
  else
    le = " NA  "
  end
  "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
end

def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body)
  reply = HTML_start + body + HTML_rest
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: text/html"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

def extract(str,pattern)
  if m = str.match(pattern)
    m[1]
  else
    nil
  end
end

# Entry:
begin
server = TCPServer.new(PORT)
loop do
  ready = IO.select([server], nil, nil, INTERVAL)
  if ready
    if session = server.accept
      request = [session.gets]  # 1st line of the request.
      request << session.gets while request[-1] =~ /\S/ # Remainder of header.
      request_type = extract(request[0],/(GET|POST) /)
      query_string = extract(request[0],/[^?]*\?(\S+)/)
      if request_type
        target = extract(request[0],/#{request_type} \/(.+?)[? ]/)
      else 
        target = nil
      end
      headers = request[1..-1].join
      content_type = extract(headers,/Content-Type:\s+(\S+)/)
      content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
      if request_type == 'POST'
        body = session.read(content_length)
      else
        body = nil
      end
      kv_pairs = [query_string,body].compact
      if kv_pairs.empty?
        params = {}
      else
        params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
          kv.split(/=/).map do |e|
            e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
          end
        end]
      end
      # Summarize:
      puts "#{request_type} #{target} received " + `date`
      puts "Content-Length: #{content_length}"
      params.each {|k,v| puts "#{k}:#{v}"}
      if request_type == "GET" and target =~ /current/i
        respond(session,log_entry)
      elsif request_type == "GET" and target =~ /last/i
        n = params["n"].to_i
        n = n < 1 ? 1 : n           # Minimum of 1.
        lines = File.readlines("temptest.txt")
        n = n > lines.size ? lines.size : n
        respond(session,lines[-n,n].join("<br>"))
      elsif request_type == "POST"
        respond(session,params.keys.map{|k| "#{k}=#{params[k]}"}.join("<br>"))
      else
        respond(session,"GET either \"current\" or \"last?n=xxx\" records.")
      end
      sleep 0.01 # Delay for a slower client.
      session.close
    end
  else # No request ready, so take and record a measurement.
    open("temptest.txt","a") do |f|
      f.puts log_entry
    end
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
Then we could test with a little HTML form:
<html>
  <head><title>temptest.html</title></head>
  <body>
    <p><a href="http://192.168.1.72:8090/current" >Current Temperature</a>
    <p><a href="http://192.168.1.72:8090/last?n=10" >Last 10 Temperatures</a>
    <p><form action="http://192.168.1.72:8090/stuff" method="POST">
         <input type="text" name="x" size="5" >
         <input type="submit">
       </form>
  </body>
</html>
or with telnet:
pbook:~ $ telnet 192.168.1.72 8090
Trying 192.168.1.72...
Connected to raspberrypi.
Escape character is '^]'.
POST /stuff HTTP
Content-Length: 6

z=1234
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 78

<html>
<head><title>temptest.html</title></head>
<body>
z=1234</body>
</html>
Connection closed by foreign host.
pbook:~ $ telnet 192.168.1.72 8090
Trying 192.168.1.72...
Connected to raspberrypi.
Escape character is '^]'.
GET /current HTTP

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 96

<html>
<head><title>temptest.html</title></head>
<body>
2013-05-13 16:28:26 18.5</body>
</html>
Connection closed by foreign host.
pbook:~ $ telnet 192.168.1.72 8090
Trying 192.168.1.72...
Connected to raspberrypi.
Escape character is '^]'.
GET /last?n=5 HTTP

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 217

<html>
<head><title>temptest.html</title></head>
<body>
2013-05-13 16:23:26 18.437
<br>2013-05-13 16:23:57 18.437
<br>2013-05-13 16:24:28 18.5
<br>2013-05-13 16:24:59 18.5
<br>2013-05-13 16:25:30 18.5
</body>
</html>
Connection closed by foreign host.
And then examine the server log:
pi@raspberrypi ~ $ ./temptest2.rb
## temptest.html
GET current received Mon May 13 15:08:15 EDT 2013
Content-Length: 0
GET last received Mon May 13 15:08:21 EDT 2013
Content-Length: 0
n:10
POST stuff received Mon May 13 15:08:31 EDT 2013
Content-Length: 4
x:zz
## telnet
POST stuff received Mon May 13 16:27:28 EDT 2013
Content-Length: 6
z:1234
GET current received Mon May 13 16:28:25 EDT 2013
Content-Length: 0
GET last received Mon May 13 16:29:15 EDT 2013
Content-Length: 0
n:5

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 2: Requirements

   "Agile" development was popularized by Dave Thomas and David Heinemeir Hansson in their Agile Web Development with Rails. The idea of ways to make the client into an active participant in the development process has had other champions as well. Two others are Philip Greenspun: Software Engineering for Internet Applications with Eve Andersson and Andrew Grumet. (Also available on-line at PhilipGreenspun.com) and Bruce Tate: From Java to Ruby: Things Every Manager Should Know.

   At the beginning of development it may be helpful to divide the requirements into core functionality and legacy before deciding which features to model first. For example, The legacy recommendations from the CDC are for a paper taped to the side of the refrigerator, where twice daily would be recorded what the temperature inside was as reflected in an LCD display on the data logger box. This would seem superfluous when the temperature can be checked from a Web browser and would add maybe 10% to the hardware costs. As Tate points out, adding some legacy features may make it easier for the client to understand and adopt the new process. Other considerations might balance the small increase in hardware costs: The client's network could go down independently of the refrigeration system and an LCD would allow continued operation. The process of handwriting a copy of the temperature and time on the paper log could be replaced by a click of a push-button next to the LCD.

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 3: Coding temptest.py

   In versions 1 and 2 of temptest.rb everything was in a loop in a single thread. In order to separate the temperature data-logger thread from the Web server thread it is important that the SD card and the 1-wire bus are accessed by only one thread at a time. For our simple problem, either monitors and mutexes are suitable. Let us try monitors, an idea way older than either Python or Ruby but implemented in both. This is also our first example using the object oriented power of the two languages.

   The coding for this week will continue to practice some regular expressions and to further understanding of the Web server function at the level of listening to a TCP socket.

#! /usr/bin/ruby
# temptest3.rb
require 'monitor'
require 'socket'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>temptest.html</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
# Constants that later may be stored on SD card and updated via the Web server:
HIGH_C = 22.0
LOW_C = 15.0
INTERVAL = 30

# Monitored classes
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < HIGH_C && t > LOW_C)
    end
    t
  end

  def time_stamp
    ds = get_temp
    if ds
      le = "%5.1f" % ds
    else
      le = " NA  "
    end
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body)
  reply = HTML_start + body + HTML_rest
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: text/html"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

def extract(str,pattern)
  if m = str.match(pattern)
    m[1]
  else
    nil
  end
end

# Entry:
begin
# Instantiate the monitored resources.
  ds = DS18B20.new
  log = LogLine.new
# Separate thread for the data-logger.
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep(INTERVAL)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if request_type
      target = extract(request[0],/#{request_type} \/(.+?)[? ]/)
    else 
      target = nil
    end
    headers = request[1..-1].join
    content_type = extract(headers,/Content-Type:\s+(\S+)/)
    content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if request_type == 'POST'
      body = session.read(content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      params = {}
    else
      params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    # Summarize:
    puts "#{request_type} #{target} received " + `date`
    puts "Content-Length: #{content_length}"
    params.each {|k,v| puts "#{k}:#{v}"}
    if request_type == "GET" and target =~ /current/i
      respond(session,ds.time_stamp)
    elsif request_type == "GET" and target =~ /last/i
      n = params["n"].to_i
      respond(session,log.tail(n).join("<br>"))
    elsif request_type == "POST"
      respond(session,params.keys.map{|k| "#{k}=#{params[k]}"}.join("<br>"))
    else
      respond(session,"GET either \"current\" or \"last?n=xxx\" records.")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
Or with just one more class to make the server code seem a little less spaghetti like:
#! /usr/bin/ruby
# temptest4.rb
require 'monitor'
require 'socket'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>temptest.html</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
# Constants that later may be stored on SD card and updated via the Web server:
HIGH_C = 22.0
LOW_C = 15.0
INTERVAL = 30

# Monitored classes
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < HIGH_C && t > LOW_C)
    end
    t
  end

  def time_stamp
    ds = get_temp
    if ds
      le = "%5.1f" % ds
    else
      le = " NA  "
    end
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    if m = str.match(pattern)
      m[1]
    else
      nil
    end
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body)
  reply = HTML_start + body + HTML_rest
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: text/html"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

# Entry:
begin
# Instantiate the monitored resources.
  ds = DS18B20.new
  log = LogLine.new
# Separate thread for the data-logger.
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep(INTERVAL)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,log.tail(n).join("<br>"))
    elsif r.request_type == "POST"
      respond(session,r.params.keys.map{|k| "#{k}=#{r.params[k]}"}.join("<br>"))
    else
      respond(session,"GET either \"current\" or \"last?n=xxx\" records.")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 4: Coding temptest.py continued

   The coding for this week will add the ability to return an image from our Web server. First we will install ImageMagick, which is well supported in both Ruby and Python. GD is a lot smaller but the documentation has fallen behind a bit. PIL would be a good alternative for Python.

## First ImageMagick
pi@raspberrypi ~ $ sudo apt-get install imagemagick
The following extra packages will be installed:
  imagemagick-common libdjvulibre-text libdjvulibre21 libexiv2-12 libilmbase6
  liblensfun-data liblensfun0 liblqr-1-0 libmagickcore5 libmagickcore5-extra
  libmagickwand5 libnetpbm10 libopenexr6 libwmf0.2-7 netpbm ufraw-batch
Suggested packages:
  imagemagick-doc autotrace enscript ffmpeg gimp gnuplot grads hp2xx html2ps
  libwmf-bin mplayer povray radiance sane-utils texlive-base-bin transfig
  exiv2 ufraw
The following NEW packages will be installed:
  imagemagick imagemagick-common libdjvulibre-text libdjvulibre21 libexiv2-12
  libilmbase6 liblensfun-data liblensfun0 liblqr-1-0 libmagickcore5
  libmagickcore5-extra libmagickwand5 libnetpbm10 libopenexr6 libwmf0.2-7
  netpbm ufraw-batch
0 upgraded, 17 newly installed, 0 to remove and 157 not upgraded.
Need to get 6,968 kB of archives.
...
## Then the Ruby bindings for ImageMagick, RMagick.
## Also need the development libraries and headers to build RMagick.
## Some of the development libraries were missing -- so first:
pi@raspberrypi ~ $ sudo apt-get update
## Now get the libraries.
pi@raspberrypi ~ $ sudo apt-get install libmagickcore-dev libmagickwand-dev
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  autotools-dev gir1.2-freedesktop gir1.2-gdkpixbuf-2.0 gir1.2-glib-2.0
  gir1.2-rsvg-2.0 javascript-common libbz2-dev libcairo-gobject2
  libcairo-script-interpreter2 libcairo2 libcairo2-dev libcdt4 libcgraph5
  libdjvulibre-dev libelf1 libexif-dev libexpat1-dev libfontconfig1-dev
  libgdk-pixbuf2.0-dev libgirepository-1.0-1 libglib2.0-0 libglib2.0-bin
  libglib2.0-dev libgraph4 libgraphviz-dev libgvc5 libgvpr1 libice-dev
  libilmbase-dev libjasper-dev libjbig-dev libjpeg8-dev libjs-jquery
  liblcms1-dev liblqr-1-0-dev libltdl-dev libopenexr-dev libpathplan4
  libpcre3-dev libpcrecpp0 libpixman-1-0 libpixman-1-dev libpng12-dev
  libpthread-stubs0 libpthread-stubs0-dev librsvg2-dev libsm-dev libtiff4-dev
  libtiffxx0c2 libtool libwmf-dev libx11-dev libx11-doc libxau-dev
  libxcb-render0 libxcb-render0-dev libxcb-shm0 libxcb-shm0-dev libxcb1
  libxcb1-dev libxdmcp-dev libxdot4 libxext-dev libxext6 libxml2 libxml2-dev
  libxrender-dev libxrender1 libxt-dev libxt6 wwwconfig-common
  x11proto-core-dev x11proto-input-dev x11proto-kb-dev x11proto-render-dev
  x11proto-xext-dev xorg-sgml-doctools xtrans-dev
Suggested packages:
  apache2 httpd libcairo2-doc libglib2.0-doc libice-doc libtool-doc
  librsvg2-doc libsm-doc autoconf automaken gfortran fortran95-compiler gcj
  libwmf-doc libxcb-doc libxext-doc libxt-doc mysql-client postgresql-client
The following NEW packages will be installed:
  autotools-dev gir1.2-freedesktop gir1.2-gdkpixbuf-2.0 gir1.2-glib-2.0
  gir1.2-rsvg-2.0 javascript-common libbz2-dev libcairo-script-interpreter2
  libcairo2-dev libcdt4 libcgraph5 libdjvulibre-dev libelf1 libexif-dev
  libexpat1-dev libfontconfig1-dev libgdk-pixbuf2.0-dev libgirepository-1.0-1
  libglib2.0-bin libglib2.0-dev libgraph4 libgraphviz-dev libgvc5 libgvpr1
  libice-dev libilmbase-dev libjasper-dev libjbig-dev libjpeg8-dev
  libjs-jquery liblcms1-dev liblqr-1-0-dev libltdl-dev libmagickcore-dev
  libmagickwand-dev libopenexr-dev libpathplan4 libpcre3-dev libpcrecpp0
  libpixman-1-dev libpng12-dev libpthread-stubs0 libpthread-stubs0-dev
  librsvg2-dev libsm-dev libtiff4-dev libtiffxx0c2 libtool libwmf-dev
  libx11-dev libx11-doc libxau-dev libxcb-render0-dev libxcb-shm0-dev
  libxcb1-dev libxdmcp-dev libxdot4 libxext-dev libxml2-dev libxrender-dev
  libxt-dev wwwconfig-common x11proto-core-dev x11proto-input-dev
  x11proto-kb-dev x11proto-render-dev x11proto-xext-dev xorg-sgml-doctools
  xtrans-dev
The following packages will be upgraded:
  libcairo-gobject2 libcairo2 libglib2.0-0 libpixman-1-0 libxcb-render0
  libxcb-shm0 libxcb1 libxext6 libxml2 libxrender1 libxt6
11 upgraded, 69 newly installed, 0 to remove and 174 not upgraded.
Need to get 2,710 kB/28.9 MB of archives.
After this operation, 64.4 MB of additional disk space will be used.
...
pi@raspberrypi ~ $ sudo gem install rmagick
Building native extensions.  This could take a while...
Successfully installed rmagick-2.13.2
1 gem installed
Installing ri documentation for rmagick-2.13.2...
Installing RDoc documentation for rmagick-2.13.2...
## Testing:
irb(main):003:0> require 'RMagick'
=> true
irb(main):004:0> Text = 'RMagick'
=> "RMagick"
irb(main):006:0* granite = Magick::ImageList.new('granite:')
=> [granite:=>GRANITE GIF 128x128 128x128+0+0 PseudoClass 16c 8-bit 6kb]
irb(main):007:0> canvas = Magick::ImageList.new
=> []
irb(main):008:0> canvas.new_image(300, 100, Magick::TextureFill.new(granite))
=> [  300x100 DirectClass 16-bit]
irb(main):010:0* text = Magick::Draw.new
=> (no primitives defined)
irb(main):011:0> text.pointsize = 52
=> 52
irb(main):012:0> text.gravity = Magick::CenterGravity
=> CenterGravity=5
irb(main):014:0* text.annotate(canvas, 0,0,2,2, Text) {
irb(main):015:1*     self.fill = 'gray83'
irb(main):016:1> }
=> (no primitives defined)
irb(main):018:0* text.annotate(canvas, 0,0,-1.5,-1.5, Text) {
irb(main):019:1*     self.fill = 'gray40'
irb(main):020:1> }
=> (no primitives defined)
irb(main):021:0> 
irb(main):022:0* text.annotate(canvas, 0,0,0,0, Text) {
irb(main):023:1*     self.fill = 'darkred'
irb(main):024:1> }
=> (no primitives defined)
irb(main):026:0* #canvas.display
irb(main):027:0* canvas.write('rubyname.gif')
=> [rubyname.gif  300x100 PseudoClass 193c 16-bit 12kb]
Also, a regular expression was ammended to be able to read negative Celsius temperatures.
#! /usr/bin/ruby
# temptest5.rb
require 'monitor'
require 'socket'
require 'RMagick'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>temptest.html</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
# Constants that later may be stored on SD card and updated via the Web server:
HIGH_C = 22.0
LOW_C = 15.0
INTERVAL = 30

# Monitored classes
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < HIGH_C && t > LOW_C)
    end
    t
  end

  def time_stamp
    ds = get_temp
    if ds
      le = "%5.1f" % ds
    else
      le = " NA  "
    end
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    if m = str.match(pattern)
      m[1]
    else
      nil
    end
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_start + body + HTML_rest
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_start + "#{mime} is not supported" + HTML_rest
  end
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: #{content_type[mime]}"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

# Entry:
begin
# Instantiate the monitored resources.
  ds = DS18B20.new
  log = LogLine.new
# Separate thread for the data-logger.
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep(INTERVAL)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,log.tail(n).join("<br>"))
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      canvas = Magick::ImageList.new "rubyname.gif"
      canvas.format = "PNG"
      respond(session,canvas.to_blob,"PNG")
    elsif r.request_type == "POST"
      respond(session,r.params.keys.map{|k| "#{k}=#{r.params[k]}"}.join("<br>"))
    else
      respond(session,"GET either \"current\" or \"last?n=xxx\" records.")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
Returning an image

   An update of our temptest.html page.

<html>
  <head><title>temptest.html</title></head>
  <body>
    <p><a href="http://192.168.1.72:8090/current" >Current Temperature</a>
    <p><a href="http://192.168.1.72:8090/last?n=10" >Last 10 Temperatures</a>
    <p><form action="http://192.168.1.72:8090/stuff" method="POST">
         <input type="text" name="x" size="5" >
         <input type="submit">
       </form>
    <p><img src="http://192.168.1.72:8090/chart.png" >
  </body>
</html>

   A version with a strip chart and an index.html response. There are of course many date formats available. In the function get_today_and_yesterday we use the default format for strptime (and we would likely use strftime and strptime in Python) in order to follow the ISO C and POSIX customs.

#! /usr/bin/ruby
# temptest6.rb
require 'monitor'
require 'socket'
require 'RMagick'
require 'date'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>Vaccine Storage Temperature</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
INDEX_HTML = <<IDX
    <h2>Remote Vaccine Storage Temperature Monitor</h2>
    <p>%s degrees Celsius</p>
    <p><img src="chart.png" ></p>
  </body>
</html>
IDX
# Constants that later may be stored on SD card and updated via the Web server:
HIGH_C = 22.0
LOW_C = 15.0
INTERVAL = 30

# Monitored classes
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < HIGH_C && t > LOW_C)
    end
    t
  end

  def time_stamp
    ds = get_temp
    if ds
      le = "%5.1f" % ds
    else
      le = " NA  "
    end
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end
  
  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / INTERVAL)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    if m = str.match(pattern)
      m[1]
    else
      nil
    end
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_start + body + HTML_rest
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_start + "#{mime} is not supported" + HTML_rest
  end
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: #{content_type[mime]}"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

def strip_chart(points,high_limit,low_limit)
  # The parameters for the dimensions of the plot.
  xpixels = 540
  ypixels = 340
  xrange = 480 # Number of tenths of an hour in 2 days.
  # yrange = 24 (-2.0 to +22.0 degrees Celsius.) 
  # Figure the scaling and translation.  Leave some of the image height
  # at the top for the title and at the bottom for the X labels.
  xoffset = 40 # pixels
  yoffset = 60 # X label space.
  y0 = ypixels - yoffset
  x0 = xoffset
  # Map [0,0] from upper left corner to lower left corner.
  yscale = -10 # 10 pixels is 1 degree Celsius.
  # xscale = 1
  i = Magick::Image.new(xpixels,ypixels,
                        Magick::HatchFill.new("white","lightcyan2"))
  gc = Magick::Draw.new
  # Draw the limit lines and X axis.
  gc.fill('transparent')
  gc.stroke('blue')
  gc.stroke_width(2)
  [high_limit,low_limit,0.0].each do |l|
    y = y0 + l * yscale
    gc.line(x0,y,x0 + xrange,y)
  end
  # Y Labels:
  # Find line height factor to center the Y labels.
  trial = Magick::Draw.new
  trial.font_weight(Magick::NormalWeight)
  trial.font_style(Magick::NormalStyle)
  trial.stroke('transparent')
  trial.fill('black')
  center_offset = trial.get_type_metrics('0123456789').height / 2
  gc.font_weight(Magick::NormalWeight)
  gc.font_style(Magick::NormalStyle)
  gc.stroke('transparent')
  gc.fill('black')
  gc.gravity(Magick::SouthWestGravity)
  [high_limit,low_limit,0.0].each do |l|
    y =  -l * yscale + yoffset - center_offset
    gc.text(0,y,l.to_s)
  end
  # X labels:
  gc.gravity(Magick::NorthGravity)
  xlabel = %w(12am 3am 6am 9am 12pm 3pm 6pm 9pm)
  [0,30 * 8].each do |d|
    8.times do |i|
      gc.text(-xpixels/2 + x0 + d + i * 30,y0 + 25,xlabel[i])
    end
  end
  gc.text(-xpixels/2 + x0 + 16 * 30,y0 + 25,xlabel[0])
  gc.text(-xpixels/2 + x0 + 4 * 30,y0 + 40,'Yesterday')
  gc.text(-xpixels/2 + x0 + 12 * 30,y0 + 40,'Today')
  gc.text(0,10,'Recent Vaccine Storage Temperature in Degrees Celsius')
  # X tics:
  gc.fill('transparent')
  gc.stroke('black')
  gc.stroke_width(1)
  17.times do |i|
    len = [15,5,5,5,10,5,5,5][i % 8]
    gc.line(x0 + i * 30,y0,x0 + i * 30,y0 + len)
  end
  # Finally plot points:
  pts = points.map {|p| [p[0] + x0,p[1] * yscale + y0]}.flatten
  gc.polyline(*pts)
  gc.draw(i)
  i.border!(1,1, "lightcyan2")
  i.format = "PNG"
  i.to_blob
end

# Entry:
begin
# Instantiate the monitored resources.
  ds = DS18B20.new
  log = LogLine.new
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep(INTERVAL)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,log.tail(n).join("<br>"))
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      points = log.get_today_and_yesterday
      respond(session,strip_chart(points,HIGH_C,LOW_C),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,INDEX_HTML % ds.time_stamp)
    elsif r.request_type == "POST"
      respond(session,r.params.keys.map{|k| "#{k}=#{r.params[k]}"}.join("<br>"))
    else
      respond(session,"GET \"current\" or \"last?n=xxx\" recorded " +
                      "or \"index.html\"")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
index.html from temptest6.rb

   To finish the interface for the ordinary user, remember we are trying to replace a record made on a piece of paper taped to the side of a refrigerator. We have already implemented a way to view the temperature status remotely from some arbitrary Web browser. Now to allow a person to initial the log sheet indicating that they have viewed the status B.I.D. (twice daily.) Most of the pieces are already in place. We need another log file that is protected from thread conflicts. We can place an arrow with the initials on the strip chart to visually indicate that the status was initialed. After processing the POST request to initial the strip the user will be redirected with a 303 See Other back to index.html. Lastly, to prepare something to be administered next week, the limits and sampling intervals are moved to limits.txt. Since the limits can be changed we could also ammend the strip chart plot height to change along with the high limit.

#! /usr/bin/ruby
# temptest7.rb
require 'monitor'
require 'socket'
require 'RMagick'
require 'date'
# Constants:
PORT = 8090
HTML_start = <<START
<html>
<head><title>Vaccine Storage Temperature</title></head>
<body>
START
HTML_rest = <<REST
</body>
</html>
REST
INDEX_HTML = <<IDX
    <h2>Remote Vaccine Storage Temperature Monitor</h2>
    <p>%s degrees Celsius</p>
    <p><img src="chart.png" ></p>
    <p><form action="initial" method="POST">
         Initials: <input type="text" name="initials" value="" size="6" >
         <input type="submit" value="Initial the Log" >
       </form>
  </body>
</html>
IDX
# Globals:
$high_c = 8.0
$low_c = 2.0
$interval = 30
# Monitored classes:
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < $high_c && t > $low_c)
    end
    t
  end

  def time_stamp
    ds = get_temp
    if ds
      le = "%5.1f" % ds
    else
      le = " NA  "
    end
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{le}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / $interval)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

end

class BidLog < Monitor
  def initialize
    super
  end

  def puts(initials)
    synchronize do
      open("bidlog.txt","a") do |f|
        f.puts "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{initials}" 
      end
    end
  end

  def get_lines
    lines = []
    synchronize do
      open("bidlog.txt") do |f|
        lines = f.readlines
      end
    end
    lines
  end

  def get_today_and_yesterday
    lines = get_lines
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday
        # Save the offset in tenths of an hour and the initials as a pair.
        records << [(dt - yesterday) / 360,tokens[2]]
      end                    
    end
    records
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    if m = str.match(pattern)
      m[1]
    else
      nil
    end
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_start + body + HTML_rest
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_start + "#{mime} is not supported" + HTML_rest
  end
  session.puts "HTTP/1.1 200 OK"
  session.puts "Content-Type: #{content_type[mime]}"
  session.puts "Content-Length: #{reply.size}"
  session.puts
  session.puts reply
end

def redirect(session,get)
  session.puts "HTTP/1.1 303 See Other"
  session.puts "Location: http://192.168.1.72:#{PORT}/#{get}.html"
  session.puts
end

def strip_chart(points,initials,high_limit,low_limit)
  # The parameters for the dimensions of the plot.
  xrange = 480 # Number of tenths of an hour in 2 days.
  yrange = (high_limit.to_i + 2) * 10
  xpixels = xrange + 60
  ypixels = yrange + 100
  # Figure the scaling and translation.  Leave some of the image height
  # at the top for the title and at the bottom for the X labels.
  xoffset = 40 # pixels
  yoffset = 60 # X label space.
  y0 = ypixels - yoffset
  x0 = xoffset
  # Map [0,0] from upper left corner to lower left corner.
  yscale = -10 # 10 pixels is 1 degree Celsius.
  # xscale = 1
  i = Magick::Image.new(xpixels,ypixels,
                        Magick::HatchFill.new("white","lightcyan2"))
  gc = Magick::Draw.new
  # Draw the limit lines and X axis.
  gc.fill('transparent')
  gc.stroke('blue')
  gc.stroke_width(2)
  [high_limit,low_limit,0.0].each do |l|
    y = y0 + l * yscale
    gc.line(x0,y,x0 + xrange,y)
  end
  # Y Labels:
  # Find line height factor to center the Y labels.
  trial = Magick::Draw.new
  trial.font_weight(Magick::NormalWeight)
  trial.font_style(Magick::NormalStyle)
  trial.stroke('transparent')
  trial.fill('black')
  center_offset = trial.get_type_metrics('0123456789').height / 2
  gc.font_weight(Magick::NormalWeight)
  gc.font_style(Magick::NormalStyle)
  gc.stroke('transparent')
  gc.fill('black')
  gc.gravity(Magick::SouthWestGravity)
  [high_limit,low_limit,0.0].each do |l|
    y =  -l * yscale + yoffset - center_offset
    gc.text(0,y,l.to_s)
  end
  # X labels:
  gc.gravity(Magick::NorthGravity)
  xlabel = %w(12am 3am 6am 9am 12pm 3pm 6pm 9pm)
  [0,30 * 8].each do |d|
    8.times do |i|
      gc.text(-xpixels/2 + x0 + d + i * 30,y0 + 25,xlabel[i])
    end
  end
  gc.text(-xpixels/2 + x0 + 16 * 30,y0 + 25,xlabel[0])
  gc.text(-xpixels/2 + x0 + 4 * 30,y0 + 40,'Yesterday')
  gc.text(-xpixels/2 + x0 + 12 * 30,y0 + 40,'Today')
  gc.text(0,10,'Recent Vaccine Storage Temperature in Degrees Celsius')
  # X tics:
  gc.fill('transparent')
  gc.stroke('black')
  gc.stroke_width(1)
  17.times do |i|
    len = [15,5,5,5,10,5,5,5][i % 8]
    gc.line(x0 + i * 30,y0,x0 + i * 30,y0 + len)
  end
  # Plot points:
  pts = points.map {|p| [p[0] + x0,p[1] * yscale + y0]}.flatten
  gc.polyline(*pts)
  # Add initials:
  gc.font_weight(Magick::NormalWeight)
  gc.font_style(Magick::NormalStyle)
  gc.stroke('transparent')
  gc.fill('green')
  gc.gravity(Magick::NorthWestGravity)
  gc.text(0,30,'Initials:')
  gc.gravity(Magick::NorthGravity)
  initials.each {|i| gc.text(-xpixels/2 +i[0] + x0,30,i[1])}
  # And some little arrows:
  gc.fill('transparent')
  gc.stroke('green')
  gc.stroke_width(1)
  initials.each do |i|
    gc.line(i[0] + x0,42,i[0] + x0,50)
    gc.line(i[0] + x0 - 3,47,i[0] + x0,50)
    gc.line(i[0] + x0 + 3,47,i[0] + x0,50)
  end
  gc.draw(i)
  i.border!(1,1, "lightcyan2")
  i.format = "PNG"
  i.to_blob
end

# Entry:
begin
# Read the globals from the SD card.
open("limits.txt") do |f|
  global_settings = f.gets.split
  $high_c = global_settings[0].to_f 
  $low_c = global_settings[1].to_f 
  $interval = global_settings[2].to_i
end
# Instantiate the monitored resources.
  ds = DS18B20.new
  log = LogLine.new
  bid = BidLog.new
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep($interval)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,log.tail(n).join("<br>"))
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      initials = bid.get_today_and_yesterday
      # For display purposes, show those more than 30 tenths of an hour apart.
      i = initials.size - 1
      while i > 0
        initials.delete_at(i-1) if initials[i][0] - 30 < initials[i-1][0]
        i -= 1
      end
      points = log.get_today_and_yesterday
      respond(session,strip_chart(points,initials,$high_c,$low_c),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,INDEX_HTML % ds.time_stamp)
    elsif r.request_type == "POST" and r.target =~ /initial/i
      initials = (r.params["initials"] || "").strip
      bid.puts(initials) if initials.size >= 2
      redirect(session,'index')
    else
      respond(session,"GET \"current\" or \"last?n=xxx\" recorded " +
                      "or \"index.html\"")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
index.html from temptest7.rb

   This week a little more practice with the string handling strengths of Python and Ruby. In Perl and Visual Basic you use the substring functions a lot. Python was the first to make it a little easier to access individual characters in a string. One strength of languages like Python and Ruby is to allow the programmer to think more in terms of lists and iterators and maps rather than loops and loop-conditions. This makes writing:

  for every-member-of-some-set
    do something
  end-loop
easier, a little more functional, a little less imperative and a lot less prone to error at the edges of the set.
Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
>>> 'hello'[0]
'h'
>>> 10[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is unsubscriptable
# Ruby 1.9.x
1.9.3p327 :001 > 'hello'[0]
 => "h" 
1.9.3p327 :002 > # In Ruby 1.8.x:
1.9.3p327 :003 >   'hello'[0,1]
 => "h" 
# Binary bit testing:
1.9.3p327 :004 > 10[0]
 => 0 
1.9.3p327 :005 > 10[1]
 => 1 
Both Ruby and Python have nice versions of split that allow full use of regular expressions. Both follow the Perl convention of the default split being on whitespace. Ruby is more in the Perl regex tradition of using the null string to split into individual characters.
>>> # Python
... 'one two'.split()
['one', 'two']
>>> 'one two'.split("")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: empty separator
>>> import re
>>> re.compile("").split('one two')
['one two']
>>> re.compile("\s+").split('one two')
['one', 'two']
# In Python list is sometimes the closer obverse of join.
# Here construct a list from a sequence -- a string in this case: 
>>> list('one two')
['o', 'n', 'e', ' ', 't', 'w', 'o']
# The Python "List Comprehension" notation:
>>> list(int(x) for x in "123")
[1, 2, 3]
>>> sum(list(int(x)**2 for x in "1234"))
30
1.9.3p327 :001 > # Ruby
1.9.3p327 :002 >   'one two'.split
 => ["one", "two"] 
1.9.3p327 :003 > 'one two'.split("")
 => ["o", "n", "e", " ", "t", "w", "o"] 

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 5: Adding Remote Administration via Web Browser

   This week we will limit our "administrating" to the one important function omitted from the index.html page -- that is to be able to review the BID log. We will have the Vaccine Storage Administrator just keep track of a single password. Preliminary practice with GNU Linux system administration:

pi@raspberrypi ~ $ sudo adduser --ingroup pi admin
Adding user `admin' ...
Adding new user `admin' (1001) with group `pi' ...
Creating home directory `/home/admin' ...
Copying files from `/etc/skel' ...
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
Changing the user information for admin
Enter the new value, or press ENTER for the default
	Full Name []: Vaccine Storage Administrator
	Room Number []: 
	Work Phone []: ### Some day this may be used to text status messages. 
	Home Phone []: 
	Other []: 
pi@raspberrypi ~ $ # password assigned was ds18b20
pi@raspberrypi ~ $ sudo grep 'admin:' /etc/shadow > admin.txt
pi@raspberrypi ~ $ sudo grep 'admin:' /etc/passwd >> admin.txt
pi@raspberrypi ~ $ cat admin.txt
admin:$6$F.ZKZxXA$JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
admin:x:1001:1000:Vaccine Storage Administrator,,,:/home/admin:/bin/bash
Testing this setup:
pi@raspberrypi ~ $ irb
irb(main):001:0> pw = open('admin.txt').gets
=> "admin:$6$F.ZKZxXA$JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
irb(main):002:0> ep = pw.split(':')[1]
=> "$6$F.ZKZxXA$JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
irb(main):003:0> parts = ep.match(/(\$[1-6]\$[^$]+)\$(.*)/)[1..2]
=> ["$6$F.ZKZxXA", "JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
irb(main):004:0> 'ds18b20'.crypt(parts[0])
=> "$6$F.ZKZxXA$JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
irb(main):005:0> 'ds18b20'.crypt(parts[0]) == parts.join('$')
=> true
Assuming that the final version of this will keep the user pi to run the server, maybe a separate copy of the password hash, which might get out of date, is not a good idea and giving pi the proper group membership would be good enough.
pi@raspberrypi ~ $ sudo ls -l /etc/shadow
-rw-r----- 1 root shadow 916 Jun  7 17:59 /etc/shadow
pi@raspberrypi ~ $ sudo gpasswd -a pi shadow
Adding user pi to group shadow
... re-login
pi@raspberrypi ~ $ grep 'admin:' /etc/shadow
admin:$6$F.ZKZxXA$JKtSDpl4ehteEO/EWWIk5NWc11vDKPORoEmhypGsHfRvr2TzeP...
Along the way this week we will separate the plotting into drawing the paper (done only when the limits change) and drawing the temperature trace (done for each request.) and try to make the code a little more readable by making a class to handle plotting. We can "clamp" the y value to simulate the pegs that limit the swing of a chart recorder stylus. The plotting still takes a few seconds. During the wait for chart.png the user could decide to go on to the administration page and thus cancel the connection from the client side. We could have this thread fail silently, but if we catch the POSIX EPIPE error (Errno::EPIPE) in the respond function we can add a note to the server log. Finally, to expedite switching from drawing lines to drawing text in RMagick we use the Ruby feature that allows the programmer to re-open any class any time.
#! /usr/bin/ruby
# temptest8.rb
require 'monitor'
require 'socket'
require 'RMagick'
require 'date'
# Constants:
PORT = 8090
HTML_WRAPPER = <<WRAP
<html>
<head><title>Vaccine Storage Temperature %s</title></head>
<body>
  <h2>Remote Vaccine Storage Temperature Monitor</h2>
  %s
</body>
</html>
WRAP
INDEX_HTML = <<IDX
    <p align="right"><a href="admin.html" >Administrative page</a></p>
    <p>%s degrees Celsius</p>
    <p><img src="chart.png" ></p>
    <p><form action="initial" method="POST">
         Initials: <input type="text" name="initials" value="" size="6" >
         <input type="submit" value="Initial the Log" >
       </form>
    </p>
IDX
ADMIN_HTML = <<ADMIN
    <p align="right"><a href="index.html" >Home page</a></p>
    <h3Administration</h3>
    <p><form action="last" method="GET">
         n: <input type="text" name="n" value="10" size="8" >
         <input type="submit" value="View Last n Log Records" >
       </form>
    </p>
    <p><form action="admin.html" method="POST">
         Password: <input type="password" name="passwd" value="" size="8" >
         <input type="submit" value="View the BID Log" >
       </form>
    </p>
    <p>%s</p>
ADMIN
# Globals:
$high_c = 8.0
$low_c = 2.0
$interval = 30
# Monitored classes:
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < $high_c && t > $low_c)
    end
    t
  end

  def time_stamp
    ds = get_temp
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{ds ? "%5.1f" % ds : " NA  "}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / $interval)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

end

class BidLog < Monitor
  def initialize
    super
  end

  def puts(initials)
    synchronize do
      open("bidlog.txt","a") do |f|
        f.puts "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{initials}" 
      end
    end
  end

  def get_lines
    lines = []
    synchronize do
      open("bidlog.txt") do |f|
        lines = f.readlines
      end
    end
    lines
  end

  def get_today_and_yesterday
    lines = get_lines
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split(nil,3) # Initials could have embedded spaces.
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday
        # Save the offset in tenths of an hour and the initials as a pair.
        records << [(dt - yesterday) / 360,tokens[2]]
      end                    
    end
    records
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    m = str.match(pattern)
    m && m[1]
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,title,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_WRAPPER % [title,body]
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_WRAPPER % ["Error","#{mime} is not supported"]
  end
  begin
    session.puts "HTTP/1.1 200 OK"
    session.puts "Content-Type: #{content_type[mime]}"
    session.puts "Content-Length: #{reply.size}"
    session.puts
    session.print reply
  rescue Errno::EPIPE
    puts "Connection closed by client"
  end
end

def redirect(session,get)
  session.puts "HTTP/1.1 303 See Other"
  session.puts "Location: http://192.168.1.72:#{PORT}/#{get}.html"
  session.puts
end

# Reopen the Magick::Draw class to make switching from lines to text easier.
class Magick::Draw
  def std_line(color='black',width=1)
    fill('transparent')
    stroke(color)
    stroke_width(width)
  end

  def std_text(color='black')
    font_weight(Magick::NormalWeight)
    font_style(Magick::NormalStyle)
    stroke('transparent')
    fill(color)
  end
end

class StripChart
  def initialize(high_limit,low_limit)
    @high_limit = high_limit
    @low_limit = low_limit
    # The parameters for the dimensions of the plot.
    xrange = 480 # Number of tenths of an hour in 2 days.
    yrange = (@high_limit.to_i + 2) * 10
    xpixels = xrange + 60
    ypixels = yrange + 100
    # Figure the scaling and translation.  Leave some of the image height
    # at the top for the title and at the bottom for the X labels.
    xoffset = 40 # pixels
    yoffset = 60 # X label space.
    @y0 = ypixels - yoffset
    @x0 = xoffset
    @xcenter = xpixels / 2
    # Map [0,0] from upper left corner to lower left corner.
    @yscale = -10 # 10 pixels is 1 degree Celsius.
    @xscale = 1
    # Prepare and save the background "paper" for the plot.
    i = Magick::Image.new(xpixels,ypixels,
                          Magick::HatchFill.new("white","lightcyan2"))
    gc = Magick::Draw.new
    # Draw the limit lines and X axis.
    gc.std_line('blue',2)
    [@high_limit,@low_limit,0.0].each do |l|
      y = @y0 + l * @yscale
      gc.line(@x0,y,@x0 + xrange,y)
    end
    # Y Labels:
    # Find line height factor to center the Y labels.
    trial = Magick::Draw.new
    trial.std_text
    center_offset = trial.get_type_metrics('0123456789').height / 2
    gc.std_text
    gc.gravity(Magick::SouthWestGravity)
    [@high_limit,@low_limit,0.0].each do |l|
      y =  -l * @yscale + yoffset - center_offset
      gc.text(0,y,l.to_s)
    end
    # X labels:
    gc.gravity(Magick::NorthGravity)
    xlabel = %w(12am 3am 6am 9am 12pm 3pm 6pm 9pm)
    [0,30 * 8].each do |d|
      8.times do |i|
        gc.text(-xpixels/2 + @x0 + d + i * 30,@y0 + 25,xlabel[i])
      end
    end
    gc.text(-xpixels/2 + @x0 + 16 * 30,@y0 + 25,xlabel[0])
    gc.text(-xpixels/2 + @x0 + 4 * 30,@y0 + 40,'Yesterday')
    gc.text(-xpixels/2 + @x0 + 12 * 30,@y0 + 40,'Today')
    gc.text(0,10,'Recent Vaccine Storage Temperature in Degrees Celsius')
    # X tics:
    gc.std_line
    17.times do |i|
      len = [15,5,5,5,10,5,5,5][i % 8]
      gc.line(@x0 + i * 30,@y0,@x0 + i * 30,@y0 + len)
    end
    # Add initials label
    gc.std_text('green')
    gc.gravity(Magick::NorthWestGravity)
    gc.text(0,30,'Initials:')
    gc.draw(i)
    i.border!(1,1, "lightcyan2")
    i.write("paper.png")
  end

  def plot(points,initials)
    points = plottable_points(points)
    initials = plottable_initials(initials) 
    i = Magick::ImageList.new "paper.png"
    gc = Magick::Draw.new
    # Plot points:
    gc.std_line
    pts = points.map {|p| [(p[0] + @x0).round,
                           (p[1] * @yscale + @y0).round]}.flatten
    gc.polyline(*pts)
    # Add initials:
    gc.std_text('green')
    gc.gravity(Magick::NorthGravity)
    initials.each {|i| gc.text(-@xcenter +i[0] + @x0,30,i[1])}
    # And some little arrows:
    gc.std_line('green')
    initials.each do |i|
      gc.line(i[0] + @x0,42,i[0] + @x0,50)
      gc.line(i[0] + @x0 - 3,47,i[0] + @x0,50)
      gc.line(i[0] + @x0 + 3,47,i[0] + @x0,50)
    end
    gc.draw(i)
    i.to_blob
  end

  def plottable_initials(initials)
    # For display purposes show initials more than 30 tenths of an hour apart.
    i = initials.size - 1
    while i > 0
      initials.delete_at(i-1) if initials[i][0] - 30 < initials[i-1][0]
      i -= 1
    end
    initials
  end

  def plottable_points(points)
    # Clamp y to edges of paper.
    # Also remove duplicate points after rounding x.
    yh = @high_limit + 5.8
    yl = @low_limit - 5.8
    pts = points.map do |p|
      p[0] = p[0].round
      p[1] = p[1] > yh ? yh : p[1]
      p[1] = p[1] < yl ? yl : p[1]
      [p[0],p[1]]
    end.uniq
#    puts "#{points.size} points."
#    puts "#{pts.size} plottable points."
#  Printed:
#  4167 points.
#  475 plottable points.
  end
end

# Entry:
begin
# Read the globals from the SD card.
open("limits.txt") do |f|
  global_settings = f.gets.split
  $high_c = global_settings[0].to_f 
  $low_c = global_settings[1].to_f 
  $interval = global_settings[2].to_i
end
# Instantiate the monitored resources.
ds = DS18B20.new
log = LogLine.new
bid = BidLog.new
# Load the "paper" for the strip chart,
# at startup now and anytime the limits are changed.
sc = StripChart.new($high_c,$low_c)
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep($interval)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,"current temperature",ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,"recent temperatures",log.tail(n).join("<br>"))
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      initials = bid.get_today_and_yesterday
      points = log.get_today_and_yesterday
      respond(session,"chart.png",sc.plot(points,initials),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,"index.html",INDEX_HTML % ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /admin\.html/i
      respond(session,"admin.html",ADMIN_HTML % "")
    elsif r.request_type == "POST" and r.target =~ /initial/i
      initials = (r.params["initials"] || "").strip
      bid.puts(initials) if initials.size >= 2
      redirect(session,'index')
    elsif r.request_type == "POST" and r.target =~ /admin\.html/i
      phash = `grep 'admin' /etc/shadow`.split(":")[1]
      pw = (r.params["passwd"] || "").strip
      if pw.crypt(phash.match(/^\$[1-6]\$[^$]+/)[0]) == phash
        bidrows = bid.get_lines.map do |l|
          cols = l.split(nil,3) # Initials might have spaces like: jmm md
          "<tr><td>#{cols[0]}<td>#{cols[1]}<td>#{cols[2]}</tr>\n"
        end
        body = "<table><tr><th>Date<th>Time<th>Initials</tr>\n" +
               bidrows.join + "</table>\n"
        respond(session,"admin.html",ADMIN_HTML % body)
      else
        respond(session,"admin.html",ADMIN_HTML % "Password error!")
      end
    else
      respond(session,"Unsupported request",
                      "#{r.target} is not a supported function!")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end
admin.html from temptest8.rb

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 6: Graphics

   This week we should decide if there is enough time and interest to try drawing the chart on the client side with JavaScript. One drawback to a JavaScript-HTML5 solution is that since this is done on the client's computer, there may be some conflicts with the client's policies. The GD library is not as widely used as ImageMagick but is easier to install. GD is much smaller than ImageMagick but does everything we need -- GD has only fixed-width fonts but that is a small matter. We could go with a solution that is server-side and already installed: GhostScript. PostScript is a text language that can be written with Python or Ruby without installing any additional libraries. Here is a version of chart.ps with just a few sample data points. The real data would be added by the server program.

%!PS
/lheight 12 def
/highlimit 22.0 def
/lowlimit 15.0 def
/zero 0.0 def
/degc {10 mul} def
/xrange 480 def
/xpts {xrange 60 add} def
/yrange {highlimit 2 add 10 mul} def
/ypts {yrange 100 add} def
/Helvetica findfont lheight scalefont setfont
% Margins
5 5 translate
% Grid
0 10 xpts {
  dup 0 moveto
  ypts lineto
} for
0 10 ypts {
  dup 0 exch moveto
  xpts exch lineto
} for
0.75 setgray
stroke
0 setgray
% Reset 0,0
40 60 translate
/ytitle {highlimit 4 add degc} def
/yinitials {highlimit 2 add degc} def
/hcenter { % x y string
  dup stringwidth pop 4 -1 roll
  exch 2 div sub % y string x
  3 -1 roll moveto show} def
% Title
xrange 2 div ytitle
(Recent Vaccine Storage Temperature in Degrees Celsius) hcenter
% Initials label
-20 yinitials (Initials:) hcenter
% x axis tics and labels
/xlabels [(12am) (3am) (6am) (9am) (12pm) (3pm) (6pm) (9pm)] def
/xticlen [15 5 5 5 10 5 5 5] def
/yday -50 def
/yhour -30 def
% Yesterday
120 yday (Yesterday) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for 
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
stroke
% Today
gsave
240 0 translate
120 yday (Today) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for
240 yhour xlabels 0 get hcenter
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
240 0 moveto 240 0 xticlen 0 get sub lineto
stroke
grestore
% Draw high limit, low limit and 0 reference lines.
[highlimit lowlimit zero]
{dup 0 exch degc moveto
xrange exch degc lineto} forall
3 setlinewidth
stroke
1 setlinewidth
% Label the reference lines.
[highlimit lowlimit zero]
{dup 5 string cvs dup stringwidth pop 2 add neg
lheight 3 div 4 -1 roll degc exch sub
moveto show} forall
% Draw arrows beneath initials.
/iarrow { % x y
  2 sub moveto currentpoint 10 sub lineto
  currentpoint 2 copy exch 3 add exch 3 add lineto
  moveto currentpoint exch 3 sub exch 3 add lineto} def
%% Data to be plotted -- can be written with Ruby or Python or whatever.
/initials [[120 (jmm)] [360 (jmm2)]] def
/pts [[0 5] [60 10] [180 20] [360 15]] def
% Plot the initials.
initials {0 get yinitials iarrow}forall
stroke
initials {aload pop exch yinitials 3 -1 roll hcenter} forall
% Plot the points.
pts 0 get aload pop degc moveto
pts 1 pts length 1 sub getinterval % All but the first point.
{aload pop degc lineto}forall
stroke
showpage
The Ruby or Python server program would then prepare the .png file like this:
$ irb 
1.9.3p327 > `gs -dBATCH -dNOPAUSE -sDEVICE=bbox chart.ps`
%%BoundingBox: 4 4 546 346
%%HiResBoundingBox: 4.500000 4.500000 545.499968 345.500005
 => "GPL Ghostscript 9.05 (2012-02-08)\nCopyright (C) 2010 Artifex..."
# Ghostscript is writing to both Stdout and Stderr -- so try combining them.
> `gs -dBATCH -dNOPAUSE -sDEVICE=bbox chart.ps 2>&1`
 => "GPL Ghostscript 9.05 (2012-02-08)\nCopyright (C) 2010 Artifex...
    done.\n%%BoundingBox: 4 4 546 346\n%%HiResBoundingBox:..."
> `gs -dBATCH -dNOPAUSE -sDEVICE=bbox chart.ps 2>&1`.split("\n").grep(/%%BoundingBox/)[0].split
 => ["%%BoundingBox:", "4", "4", "546", "346"] 
> bb = `gs -dBATCH -dNOPAUSE -sDEVICE=bbox chart.ps 2>&1`.split("\n").
        grep(/%%BoundingBox/)[0].split[1..-1].map {|e| e.to_i}
 => [4, 4, 546, 346] 
> gs_cmd = "gs -dBATCH -dNOPAUSE -sDEVICE=pnggray -g%dx%d " +
>          "-sOutputFile=chart.png chart.ps"
> exec gs_cmd % [bb[0] + bb[2],bb[1] + bb[3]]
GPL Ghostscript 9.05 (2012-02-08)
Copyright (C) 2010 Artifex Software, Inc... done.
chart.png from chart.ps

   Putting this together we have the constant part of the Postscript in the file:

% paper.ps Fixed part of program -- the paper with the grid and labels:
/zero 0.0 def
/degc {10 mul} def
/xrange 480 def
/xpts {xrange 60 add} def
/yrange {highlimit degc} def
/ypts {yrange 120 add} def
/lheight 12 def
/Helvetica findfont lheight scalefont setfont
% Margins
5 5 translate
% Grid
0 10 xpts {
  dup 0 moveto
  ypts lineto
} for
0 10 ypts {
  dup 0 exch moveto
  xpts exch lineto
} for
0.75 setgray
stroke
0 setgray
% Reset 0,0
40 60 translate
/ytitle {highlimit 4 add degc} def
/yinitials {highlimit 2 add degc} def
/hcenter { % x y string
  dup stringwidth pop 4 -1 roll
  exch 2 div sub % y string x
  3 -1 roll moveto show} def
% Title
xrange 2 div ytitle
(Recent Vaccine Storage Temperature in Degrees Celsius) hcenter
% Initials label
-20 yinitials (Initials:) hcenter
% x axis tics and labels
/xlabels [(12am) (3am) (6am) (9am) (12pm) (3pm) (6pm) (9pm)] def
/xticlen [15 5 5 5 10 5 5 5] def
/yday -50 def
/yhour -30 def
% Yesterday
120 yday (Yesterday) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for 
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
stroke
% Today
gsave
240 0 translate
120 yday (Today) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for
240 yhour xlabels 0 get hcenter
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
240 0 moveto 240 0 xticlen 0 get sub lineto
stroke
grestore
% Draw high limit, low limit and 0 reference lines.
[highlimit lowlimit zero]
{dup 0 exch degc moveto
xrange exch degc lineto} forall
3 setlinewidth
stroke
1 setlinewidth
% Label the reference lines.
[highlimit lowlimit zero]
{dup 5 string cvs dup stringwidth pop 2 add neg
lheight 3 div 4 -1 roll degc exch sub
moveto show} forall
% Draw arrows beneath initials.
/iarrow { % x y
  2 sub moveto currentpoint 10 sub lineto
  currentpoint 2 copy exch 3 add exch 3 add lineto
  moveto currentpoint exch 3 sub exch 3 add lineto} def
% Plot the initials.
initials length 0 gt {
  initials {0 get yinitials iarrow}forall
  stroke
  initials {aload pop exch yinitials 3 -1 roll hcenter} forall }if
% Plot the points.
pts length 0 gt {
  pts 0 get aload pop degc moveto
  pts 1 pts length 1 sub getinterval % All but the first point.
  {aload pop degc lineto}forall
  stroke}if
showpage
And the revised server program:
#! /usr/bin/ruby
# temptest9.rb
require 'monitor'
require 'socket'
require 'date'
# Constants:
PORT = 8090
HTML_WRAPPER = <<WRAP
<html>
<head><title>Vaccine Storage Temperature %s</title></head>
<body>
  <h2>Remote Vaccine Storage Temperature Monitor</h2>
  %s
</body>
</html>
WRAP
INDEX_HTML = <<IDX
    <p align="right"><a href="admin.html" >Administrative page</a></p>
    <p>%s degrees Celsius</p>
    <p><img src="chart.png" ></p>
    <p><form action="initial" method="POST">
         Initials: <input type="text" name="initials" value="" size="6" >
         <input type="submit" value="Initial the Log" >
       </form>
    </p>
IDX
ADMIN_HTML = <<ADMIN
    <p align="right"><a href="index.html" >Home page</a></p>
    <h3Administration</h3>
    <p><form action="last" method="GET">
         n: <input type="text" name="n" value="10" size="8" >
         <input type="submit" value="View Last n Log Records" >
       </form>
    </p>
    <p><form action="admin.html" method="POST">
         Password: <input type="password" name="passwd" value="" size="8" >
         <input type="submit" value="View the BID Log" >
       </form>
    </p>
    <p>%s</p>
ADMIN
# Globals:
$high_c = 8.0
$low_c = 2.0
$interval = 30
# Monitored classes:
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < $high_c && t > $low_c)
    end
    t
  end

  def time_stamp
    ds = get_temp
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{ds ? "%5.1f" % ds : " NA  "}" 
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / $interval)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

end

class BidLog < Monitor
  def initialize
    super
  end

  def puts(initials)
    synchronize do
      open("bidlog.txt","a") do |f|
        f.puts "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{initials}" 
      end
    end
  end

  def get_lines
    lines = []
    synchronize do
      open("bidlog.txt") do |f|
        lines = f.readlines
      end
    end
    lines
  end

  def get_today_and_yesterday
    lines = get_lines
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split(nil,3) # Initials could have embedded spaces.
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday
        # Save the offset in tenths of an hour and the initials as a pair.
        records << [(dt - yesterday) / 360,tokens[2]]
      end                    
    end
    records
  end

end

# Class to extract a request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    m = str.match(pattern)
    m && m[1]
  end

end

# Subroutines called by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end

def respond(session,title,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_WRAPPER % [title,body]
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_WRAPPER % ["Error","#{mime} is not supported"]
  end
  begin
    session.puts "HTTP/1.1 200 OK"
    session.puts "Content-Type: #{content_type[mime]}"
    session.puts "Content-Length: #{reply.size}"
    session.puts
    session.print reply
  rescue Errno::EPIPE
    puts "Connection closed by client"
  end
end

def redirect(session,get)
  session.puts "HTTP/1.1 303 See Other"
  session.puts "Location: http://192.168.1.72:#{PORT}/#{get}.html"
  session.puts
end

class StripChart
  def initialize(high_limit,low_limit)
    @high_limit = high_limit
    @low_limit = low_limit
    @bbox = [480 + 60 + 10,(@high_limit + 12) * 10 + 10]
  end

  def plot(points,initials)
    points = plottable_points(points)
    pts_def = "/pts [" +
              points.map {|p| "[#{p[0]} #{p[1]}]"}.join(' ') +
              "] def"
    initials = plottable_initials(initials)
    initials_def = "/initials [" +
                   initials.map {|p| "[#{p[0]} (#{p[1]})]"}.join(' ') +
                   "] def"
    open("chart.ps","w") do |f|
      f.puts "%!PS"
      f.puts "/highlimit %.1f def" % @high_limit
      f.puts "/lowlimit %.1f def" % @low_limit
      f.puts pts_def
      f.puts initials_def
      f.puts open("paper.ps").read
    end
    gs_cmd = "gs -dBATCH -dNOPAUSE -sDEVICE=pnggray -g%dx%d " +
             "-sOutputFile=chart.png chart.ps 2>&1"
    puts `#{gs_cmd % @bbox}`
    png = open("chart.png","rb").read
    puts "PNG is #{png.size} bytes."
    png
  end

  def plottable_initials(initials)
    # For display purposes show initials more than 30 tenths of an hour apart.
    i = initials.size - 1
    while i > 0
      initials.delete_at(i-1) if initials[i][0] - 30 < initials[i-1][0]
      i -= 1
    end
    initials
  end

  def plottable_points(points)
    # Clamp y to edges of paper.
    # Also remove duplicate points after rounding x.
    yh = @high_limit + 5.8
    yl = @low_limit - 5.8
    pts = points.map do |p|
      p[0] = p[0].round
      p[1] = p[1] > yh ? yh : p[1]
      p[1] = p[1] < yl ? yl : p[1]
      [p[0],p[1]]
    end.uniq
#    puts "#{points.size} points."
#    puts "#{pts.size} plottable points."
#  Printed:
#  4167 points.
#  475 plottable points.
  end
end

# Entry:
begin
# Read the globals from the SD card.
open("limits.txt") do |f|
  global_settings = f.gets.split
  $high_c = global_settings[0].to_f 
  $low_c = global_settings[1].to_f 
  $interval = global_settings[2].to_i
end
# Instantiate the monitored resources.
ds = DS18B20.new
log = LogLine.new
bid = BidLog.new
# Load the "paper" for the strip chart,
# at startup now and anytime the limits are changed.
sc = StripChart.new($high_c,$low_c)
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep($interval)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /current/i
      respond(session,"current temperature",ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,"recent temperatures",log.tail(n).join("<br>"))
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      initials = bid.get_today_and_yesterday
      points = log.get_today_and_yesterday
      respond(session,"chart.png",sc.plot(points,initials),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,"index.html",INDEX_HTML % ds.time_stamp)
    elsif r.request_type == "GET" and r.target =~ /admin\.html/i
      respond(session,"admin.html",ADMIN_HTML % "")
    elsif r.request_type == "POST" and r.target =~ /initial/i
      initials = (r.params["initials"] || "").strip
      bid.puts(initials) if initials.size >= 2
      redirect(session,'index')
    elsif r.request_type == "POST" and r.target =~ /admin\.html/i
      phash = `grep 'admin' /etc/shadow`.split(":")[1]
      pw = (r.params["passwd"] || "").strip
      if pw.crypt(phash.match(/^\$[1-6]\$[^$]+/)[0]) == phash
        bidrows = bid.get_lines.map do |l|
          cols = l.split(nil,3) # Initials might have spaces like: jmm md
          "<tr><td>#{cols[0]}<td>#{cols[1]}<td>#{cols[2]}</tr>\n"
        end
        body = "<table><tr><th>Date<th>Time<th>Initials</tr>\n" +
               bidrows.join + "</table>\n"
        respond(session,"admin.html",ADMIN_HTML % body)
      else
        respond(session,"admin.html",ADMIN_HTML % "Password error!")
      end
    else
      respond(session,"Unsupported request",
                      "#{r.target} is not a supported function!")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end

   Note: As a long time user of earlier (pre 1.9.x) versions of Ruby and using some version of Unix, I had not used the binary flag to open a binary file for a while. In Ruby 1.9.x the improved support for multibyte characters has made the binary flag important again.

pi@raspberrypi ~ $ irb
irb(main):001:0> f = open("chart.png")
=> #<File:chart.png>
irb(main):002:0> f.external_encoding.name
=> "UTF-8"
irb(main):003:0> content = f.read
irb(main):004:0> content.encoding.name
=> "UTF-8"
irb(main):005:0> content.size
=> 3765
irb(main):006:0> f.size
=> 3898
irb(main):007:0> `ls -l chart.png`
=> "-rw-r--r-- 1 pi pi 3898 Jun 21 21:22 chart.png\n"
irb(main):008:0> f.close
irb(main):009:0> f = open("chart.png","rb")
=> #<File:chart.png>
irb(main):010:0> f.external_encoding.name
=> "ASCII-8BIT"
irb(main):011:0> content = f.read
irb(main):012:0> content.size
=> 3898
irb(main):013:0> f.size
=> 3898
irb(main):014:0> content.encoding.name
=> "ASCII-8BIT"
irb(main):015:0> f.external_encoding.name
=> "ASCII-8BIT"

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 7: Testing

   This week we will give some thought to functional unit testing of our code. However we also want to consider another administrative function: testing to see if the system is still working. Logging a comparison to some other thermometer on a weekly or monthly basis would help. We have 3 columns in the initials log now: Date, Time and Initials We could expand that to 5: Date, Time, Pi DS18B20 Temperature, Additional Thermometer Temperature and Initials.

   To test our administrative password system we could just type the password into a Web browser. An automated unit-test would be better. First a little irb practice with an HTTP client:

pi@raspberrypi ~ $ irb
> require 'net/http'
=> true
> Net::HTTP.start("localhost",8090) do |http|
> response = http.get("/index.html")
  response.each {|k,v| puts "#{k}:#{v}"}
> end
content-type:text/html
content-length:511
> Net::HTTP.start("localhost",8090) do |http|
> response = http.post("/admin.html","passwd=ds18b20")
> response.each {|k,v| puts "#{k}:#{v}"}
> puts response.body
> end
content-type:text/html
content-length:1411
<html>
<head><title>Vaccine Storage Temperature admin.html</title></head>
<body>
...
    </p>
    <p><table><tr><th>Date<th>Time<th>Initials</tr>
<tr><td>2013-06-03<td>16:14:56<td>jmm</tr>
...
</table>
...
> Net::HTTP.start("localhost",8090) do |http|
> response = http.post("/admin.html","passwd=false")
> response.each {|k,v| puts "#{k}:#{v}"}
> puts response.body
> end
content-type:text/html
content-length:665
<html>
...
    <p>Password error!</p>
...
Then we can write a test suite:
# test_temptest.rb
require 'test/unit'
require 'net/http'
class TestPasswd < Test::Unit::TestCase

def setup
  # Should use text fixture data, but will just use our real files for now.
end

def teardown
  # Ditto.
end

def test_good_password
  Net::HTTP.start("localhost",8090) do |http|
    response = http.post("/admin.html","passwd=ds18b20")
    assert_match(/<table>/,response.body,"No table returned")
    assert_no_match(/Password error!/,response.body,"Password error")
  end
end

def test_bad_password
  Net::HTTP.start("localhost",8090) do |http|
    response = http.post("/admin.html","passwd=fake")
    assert_no_match(/<table>/,response.body,"Table returned")
    assert_match(/Password error!/,response.body,"No Password error")
  end
end

end
Which runs like this:
pi@raspberrypi ~ $ ruby test_temptest.rb 
Run options: 
# Running tests:
..
Finished tests in 0.420112s, 4.7606 tests/s, 14.2819 assertions/s.
2 tests, 6 assertions, 0 failures, 0 errors, 0 skips

We might make a tentative design for the Web page where the results from a thermometer not connected to the Raspberry Pi could be entered. Perhaps this would be a simple addition to the "Initial the Strip" function. However, we should concentrate on our job which is to make the task of the person who monitors the vaccine temperature easier and more accurate. In the handouts from VFC, calibrating against another thermometer is left as a task for an outside agency. Drift does not seem to be a big problem with the digital sensor. Instead of checking for drift, we could perhaps help with ongoing testing and quality assurance by posting a warning on the main page if the DS18B20 reading remains invalid for more than 1 minute or does not change at all over 60 minutes. This would mean maybe adding three new instance attributes to the class that manages the DS18B20: @last_valid_reading and @last_valid_time and @last_different_reading_time. The full 4 decimal place result in degrees Centigrade should be enough to represent all the bits down to the 2-4 resolution of the DS18B20. The Unix time (in seconds) would be enough for the time stamps. The pseudo-code might be something like:

get-reading
if reading is valid
  @last_valid_time <- current-time
  if reading != @last_valid_reading
    @last_valid_reading <- reading
    @last_different_reading_time <- current
if @last_different_reading_time - current > 1 hour
  warn stuck!  
if @last_valid_time - current > 10 minutes
  warn broken!

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 8: Improvements

   This week is 100% on-line! Examples of improvements might be to display a little more information on the main page, in addition to the current time and temperature:

  1. Minimum and maximum temperatures in past 24 hours.
  2. Average temperature in past 24 hours.
  3. Time out of range in past 24 hours.

   Along with the relatively small changes for the past two weeks, there has also been a program name change and a little code reorganization. The name change has been to get ready for next week's discussion of deployment. The code reorganization was to collect the parts that produce the user's view of our server program.

#! /usr/bin/ruby
# vmonitor.rb
require 'monitor'
require 'socket'
require 'date'
# Constants:
PORT = 8090
STUCK = 3600 # 1 hour in seconds.
BROKEN = 600 # 10 minutes in seconds.
# Globals:
$high_c = 8.0
$low_c = 2.0
$interval = 30
# Monitored classes:
class DS18B20 < Monitor

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    @last_valid_time = Time.now
    @last_different_reading_time = @last_valid_time
    @last_valid_reading = nil
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < $high_c && t > $low_c)
    end
    # Status updates:
    if t
      @last_valid_time = Time.now
      if !@last_valid_reading or t != @last_valid_reading
        @last_valid_reading = t
        @last_different_reading_time = Time.now
      end
    end
    t
  end

  def time_stamp
    ds = get_temp
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{ds ? "%5.1f" % ds : " NA  "}" 
  end

  def warnings
    w = ""
    if Time.now - @last_different_reading_time > STUCK # 1 hour in seconds. 
      w += "Temperature stuck at same reading for more than 1 hour.  "
    end
    if Time.now - @last_valid_time > BROKEN # 10 minutes in seconds.
      w += "No temperature readings available for more than 10 minutes.  "
    end
    if @last_valid_reading and @last_valid_reading > $high_c 
      w += "Storage temperature is too hot.  "
    elsif @last_valid_reading and @last_valid_reading < $low_c 
      w += "Storage temperature is too cold.  "
    end
    w = '<font color="red">' + w + '</font>' if w !~ /^\s*$/
    w
  end

end  

class LogLine < Monitor
  LineLength = 26
  def initialize
    super
  end

  def puts(line)
    synchronize do
      open("templog.txt","a") do |f|
        f.puts line
      end
    end
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open("templog.txt") do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / $interval)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

  def last_24h_summary
    h24 = 24 * 60 * 60
    lines = tail(h24 / $interval)
    records = []
    yesterday = Time.now - h24
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      records << tokens[2].to_f if dt >= yesterday and tokens[2] =~ /^[-0-9]/
    end
    "Temperature over the last 24 hours: " +
    "Average #{"%.1f" % (records.reduce(:+) / records.size)}, " +
    "Hign #{"%.1f" % records.max}, " +
    "Low #{"%.1f" % records.min}, " +
    "Minutes out of range " +
    "#{records.select{|r| r > $high_c or r < $low_c }.size * $interval / 60}"
  end

end

class BidLog < Monitor
  def initialize
    super
  end

  def puts(initials)
    synchronize do
      open("bidlog.txt","a") do |f|
        f.puts "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{initials}" 
      end
    end
  end

  def get_lines
    lines = []
    synchronize do
      open("bidlog.txt") do |f|
        lines = f.readlines
      end
    end
    lines
  end

  def get_today_and_yesterday
    lines = get_lines
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split(nil,3) # Initials could have embedded spaces.
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday
        # Save the offset in tenths of an hour and the initials as a pair.
        records << [(dt - yesterday) / 360,tokens[2]]
      end                    
    end
    records
  end

end
# Other subroutines and classes used by only 1 thread:
# Class to extract an HTTP request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    m = str.match(pattern)
    m && m[1]
  end

end

def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end
# Server response subroutines and templates:
HTML_WRAPPER = <<WRAP
<html><!-- template(title,heading,stats,page) -->
<head><title>Vaccine Storage Temperature <!-- title -->%s</title></head>
<body>
  <h2>Remote Vaccine Storage Temperature Monitor</h2>
  <!-- heading -->%s
  <p><!-- stats -->%s</p>
  <!-- page template -->%s
</body>
</html>
WRAP
INDEX_HTML = <<IDX
  <p align="right"><a href="admin.html" >Administrative page</a></p>
  <p><img src="chart.png" ></p>
  <p><form action="initial" method="POST">
       Initials: <input type="text" name="initials" value="" size="6" >
       <input type="submit" value="Initial the Log" >
  </form></p>
IDX
ADMIN_HEADING = "<h3>Administrative Functions</h3>\n"
ADMIN_HTML = <<ADMIN
  <!-- Admin user page(unprotected-content,protected-content) -->
  <p align="right"><a href="index.html" >Home page</a></p>
  <p><form action="last" method="GET">
      n: <input type="text" name="n" value="10" size="8" >
      <input type="submit" value="View Last n Log Records" >
  </form></p>
  <p><!-- unprotected-content -->%s</p>
  <p><form action="admin.html" method="POST">
      Password: <input type="password" name="passwd" value="" size="8" >
      <input type="submit" value="View the BID Log" >
  </form></p>
  <p><!-- protected-content -->%s</p>
ADMIN

def stats(ds,log)
  [ds.time_stamp,ds.warnings,log.last_24h_summary].select {|s| s !~ /^\s*$/}
end
  
def respond(session,title,heading,status,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_WRAPPER % [title,heading,status.join("<br>"),body]
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_WRAPPER % ["error","","","#{mime} is not supported"]
  end
  begin
    session.puts "HTTP/1.1 200 OK"
    session.puts "Content-Type: #{content_type[mime]}"
    session.puts "Content-Length: #{reply.size}"
    session.puts
    session.print reply
  rescue Errno::EPIPE
    puts "Connection closed by client"
  end
end

def redirect(session,get)
  session.puts "HTTP/1.1 303 See Other"
  session.puts "Location: http://192.168.1.72:#{PORT}/#{get}.html"
  session.puts
end

class StripChart
  def initialize(high_limit,low_limit)
    @high_limit = high_limit
    @low_limit = low_limit
    @bbox = [480 + 60 + 10,(@high_limit + 12) * 10 + 10]
  end

  def plot(points,initials)
    points = plottable_points(points)
    pts_def = "/pts [" +
              points.map {|p| "[#{p[0]} #{p[1]}]"}.join(' ') +
              "] def"
    initials = plottable_initials(initials)
    initials_def = "/initials [" +
                   initials.map {|p| "[#{p[0]} (#{p[1]})]"}.join(' ') +
                   "] def"
    open("chart.ps","w") do |f|
      f.puts "%!PS"
      f.puts "/highlimit %.1f def" % @high_limit
      f.puts "/lowlimit %.1f def" % @low_limit
      f.puts pts_def
      f.puts initials_def
      f.puts open("paper.ps").read
    end
    gs_cmd = "gs -dBATCH -dNOPAUSE -sDEVICE=pnggray -g%dx%d " +
             "-sOutputFile=chart.png chart.ps 2>&1"
    puts `#{gs_cmd % @bbox}`
    png = open("chart.png","rb").read
    puts "PNG is #{png.size} bytes."
    png
  end

  def plottable_initials(initials)
    # For display purposes show initials more than 30 tenths of an hour apart.
    i = initials.size - 1
    while i > 0
      initials.delete_at(i-1) if initials[i][0] - 30 < initials[i-1][0]
      i -= 1
    end
    initials
  end

  def plottable_points(points)
    # Clamp y to edges of paper.
    # Also remove duplicate points after rounding x.
    yh = @high_limit + 5.8
    yl = @low_limit - 5.8
    pts = points.map do |p|
      p[0] = p[0].round
      p[1] = p[1] > yh ? yh : p[1]
      p[1] = p[1] < yl ? yl : p[1]
      [p[0],p[1]]
    end.uniq
#    puts "#{points.size} points."
#    puts "#{pts.size} plottable points."
#  Printed:
#  4167 points.
#  475 plottable points.
  end
end

# Entry:
begin
# Read the globals from the SD card.
open("limits.txt") do |f|
  global_settings = f.gets.split
  $high_c = global_settings[0].to_f 
  $low_c = global_settings[1].to_f 
  $interval = global_settings[2].to_i
end
# Instantiate the monitored resources.
ds = DS18B20.new
log = LogLine.new
bid = BidLog.new
# Load the "paper" for the strip chart,
# at startup now and anytime the limits are changed.
sc = StripChart.new($high_c,$low_c)
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep($interval)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
addr = server.addr
addr.shift
puts "Server started on #{addr.join(":")}"
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    puts r.summary
    if r.request_type == "GET" and r.target =~ /last/i
      n = r.params["n"].to_i
      respond(session,"admin.html","Administrative Functions",stats(ds,log),
              ADMIN_HTML % [log.tail(n).join("<br>"),""])
    elsif r.request_type == "GET" and r.target =~ /chart\.png/i
      initials = bid.get_today_and_yesterday
      points = log.get_today_and_yesterday
      respond(session,"chart.png",nil,nil,sc.plot(points,initials),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,"index.html","",stats(ds,log),INDEX_HTML)
    elsif r.request_type == "GET" and r.target =~ /admin\.html/i
      respond(session,"admin.html",ADMIN_HEADING,stats(ds,log),
              ADMIN_HTML % ["",""])
    elsif r.request_type == "POST" and r.target =~ /initial/i
      initials = (r.params["initials"] || "").strip
      bid.puts(initials) if initials.size >= 2
      redirect(session,'index')
    elsif r.request_type == "POST" and r.target =~ /admin\.html/i
      phash = `grep 'admin' /etc/shadow`.split(":")[1]
      pw = (r.params["passwd"] || "").strip
      if pw.crypt(phash.match(/^\$[1-6]\$[^$]+/)[0]) == phash
        bidrows = bid.get_lines.map do |l|
          cols = l.split(nil,3) # Initials might have spaces like: jmm md
          "<tr><td>#{cols[0]}<td>#{cols[1]}<td>#{cols[2]}</tr>\n"
        end
        body = "<table><tr><th>Date<th>Time<th>Initials</tr>\n" +
               bidrows.join + "</table>\n"
        respond(session,"admin.html",ADMIN_HEADING,
                stats(ds,log),ADMIN_HTML % ["",body])
      else
        respond(session,"admin.html",ADMIN_HEADING,stats(ds,log),
                ADMIN_HTML % ["","Password error!"])
      end
    else
      respond(session,"not-supported.html","",[""],
              "Error: #{r.target} is not a supported function!")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
end

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 9: Deployment

   Two aspects of deployment:

Software
Hardware

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Week 10: Report

   Subtopics:

   Wrapping up issues:

#! /usr/bin/ruby
# vmonitor.rb
require 'monitor'
require 'socket'
require 'date'
require 'fileutils'
# Constants:
PORT = 8090
STUCK = 3600 # 1 hour in seconds.
BROKEN = 600 # 10 minutes in seconds.
WDIR = "/home/pi/"
TEMPLOG = 'templog'
BIDLOG = 'bidlog'
TEMPLOG_LINE_LENGTH = 26
# Globals:
$high_c = 8.0
$low_c = 2.0
$interval = 30
# Monitored classes:
class DS18B20 < Monitor
  attr_reader :lcd

  def initialize
    @device = Dir["/sys/bus/w1/devices/28*"][0]
    @last_valid_time = Time.now
    @last_different_reading_time = @last_valid_time
    @last_valid_reading = nil
    @broken = false
    @stuck = false
    @lcd = BackpackLCD.new
    super
  end

  def get_temp
    t = nil
    synchronize do
      w1 = open(@device + "/w1_slave")
      if w1.gets =~ /YES/
        t = w1.gets.match(/t=(-?\d{4,6})/)[1].to_f * 0.001
      end
      w1.close
      # Adjust the Monitor On and In Range LED.
      green(t && t < $high_c && t > $low_c)
      # Update the LCD display.
      @lcd.print_at(0,0,"%-20s" % (Time.now.strftime("%m/%d %H:%M") + 
                        ( t ? "%5.1f\x01C" % t : " N/A ")))
      if @broken or @stuck
        lcd_warnings = "%-20s" % "Check Monitor Now!"
      elsif t and t > $high_c
        lcd_warnings = "%-20s" % "Vaccine is too hot!"
      elsif t and t < $low_c
        lcd_warnings = "%-20s" % "Vaccine is too cold!"
      else
        lcd_warnings = " " * 20
      end
      @lcd.print_at(1,0,lcd_warnings)
    end
    # Status updates:
    if t
      @last_valid_time = Time.now
      if !@last_valid_reading or t != @last_valid_reading
        @last_valid_reading = t
        @last_different_reading_time = Time.now
      end
    end
    t
  end

  def time_stamp
    ds = get_temp
    "#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{ds ? "%5.1f" % ds : " NA  "}" 
  end

  def clear_severe
    @stuck = false
    @broken = false
  end

  def severe_warnings
    sw = ""
    sw += "Broken #{@broken.strftime "%Y-%m-%d %H:%M:%S"}" if @broken
    sw += "Stuck #{@stuck.strftime "%Y-%m-%d %H:%M:%S"}" if @stuck
    sw
  end

  def warnings
    w = ""
    if Time.now - @last_different_reading_time > STUCK or @stuck
      @stuck ||= Time.now
      w += "Temperature stuck at same reading for more than 1 hour.  "
    end
    if Time.now - @last_valid_time > BROKEN or @broken
      @broken ||= Time.now
      w += "No temperature readings available for more than 10 minutes.  "
    end
    if @last_valid_reading and @last_valid_reading > $high_c 
      w += "Storage temperature is too hot.  "
    elsif @last_valid_reading and @last_valid_reading < $low_c 
      w += "Storage temperature is too cold.  "
    end
    w = '<font color="red">' + w + '</font>' if w !~ /^\s*$/
    w
  end

end

class LogMonitor < Monitor
  def initialize(logfile)
    @logfile = logfile
    super()
  end

  def puts(line)
    synchronize do
      open(WDIR + @logfile + '.txt',"a") do |f|
        f.puts line
      end
    end
  end

  def get_lines
    lines = []
    synchronize do
      open(WDIR + @logfile + '.txt') do |f|
        lines = f.readlines
      end
    end
    lines
  end

  def tail(lines = 1)
    all_lines = get_lines
    # Ensure the request is sane.
    lines = lines < 1 ? 1 : lines
    lines = lines > all_lines.size ? all_lines.size : lines
    all_lines[all_lines.size - lines,lines]
  end

  def latest
    lf = Dir[WDIR + @logfile + "-*"]
    if lf.size > 0
      lfile_name = lf.sort_by {|f| File.new(f).mtime}[-1]
      last_rotation = File.new(lfile_name).mtime.to_s
    else
      lfile_name = @logfile + '.txt'
      last_rotation = 'Never'
    end
    [lfile_name,last_rotation]
  end

  def rotate
    lf = latest
    m = lf[0].match(/-(\d{1,6})/)
    if m
      newname = @logfile + '-' + m[1].succ
    else
      newname = @logfile + '-1'
    end
    synchronize do
      File.rename(WDIR + @logfile + '.txt',WDIR + newname)
      FileUtils.touch(WDIR + @logfile + '.txt')
    end
  end
    
end

class TempLog < LogMonitor
  LineLength = TEMPLOG_LINE_LENGTH
  def initialize(logfile)
    super
  end

  def tail(lines = 1)
    last_lines = []
    synchronize do
      open(WDIR + @logfile + '.txt') do |f|
        # Ensure the request is sane.
        all_lines = f.size / LineLength
        lines = lines < 1 ? 1 : lines
        lines = lines > all_lines ? all_lines : lines
        # Count back from the end.
        f.seek(-LineLength * lines, IO::SEEK_END) 
        last_lines = f.readlines
      end
    end
    last_lines
  end

  def get_today_and_yesterday
    lines = tail(48 * 60 * 60 / $interval)
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday and tokens[2] =~ /^[-0-9]/
        # Save the offset in tenths of an hour and the temperature as a pair.
        records << [(dt - yesterday) / 360,tokens[2].to_f]
      end                    
    end
    records
  end

  def last_24h_summary
    h24 = 24 * 60 * 60
    lines = tail(h24 / $interval)
    records = []
    yesterday = Time.now - h24
    first_entry = true
    span = 24
    lines.each do |l|
      tokens = l.split
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      records << tokens[2].to_f if dt >= yesterday and tokens[2] =~ /^[-0-9]/
      if first_entry
        first_entry = false
        available_span = ((Time.now - dt) / (60 * 60)).round
        span = available_span < 24 ? available_span : 24
      end
    end
    "Temperature over the last #{span} hours: " +
    "Average #{"%.1f" % (records.reduce(:+) / records.size)}, " +
    "Hign #{"%.1f" % records.max}, " +
    "Low #{"%.1f" % records.min}, " +
    "Minutes out of range " +
    "#{records.select{|r| r > $high_c or r < $low_c }.size * $interval / 60}"
  end
end

class BidLog < LogMonitor
  def initialize(logfile)
    super
  end

  def puts(initials,status = "")
    status = "==> " + status unless status.empty?
    super("#{Time.now.strftime "%Y-%m-%d %H:%M:%S"} #{initials} #{status}")
  end

  def get_today_and_yesterday
    lines = get_lines
    records = []
    yesterday = (Date.today - 1).to_time
    lines.each do |l|
      i = l.split(/==> /)
      tokens = i[0].split(nil,3) # Initials could have embedded spaces.
      tz = Time.now.strftime('%:z')
      dt = DateTime.strptime(tokens[0] + 'T' + tokens[1] + tz).to_time
      if dt >= yesterday
        # Save the offset in tenths of an hour and the initials as a pair.
        records << [(dt - yesterday) / 360,tokens[2]]
      end                    
    end
    records
  end

end
# Other subroutines and classes used by only 1 thread:
def green(state)
  open("/sys/class/gpio/gpio17/value","w") {|f| f.puts(state ? "1" : "0")}
end
class BackpackLCD
  ## Adafruit LCD backpack:
  # Connected to SCL and SCA on the Adafruit PI Plate.
  # 4-row x 20 character LCD in 4 bit mode via MCP23008 I2C serial interface.
  # MCP23008   LCD (HD44780)
  #    GP0        ---
  #    Ground     RW (write only)
  #    GP1        RS
  #    GP2        E
  #    GP3        D4
  #    GP4        D5
  #    GP5        D6
  #    GP6        D7
  #    GP7        Display Backlight
  ## Dependencies:
  # (New modules added for this class.)
  # pi@raspberrypi ~ $ cat /etc/modules
  # ...
  # i2c-dev
  # i2c-bcm2708
  # (To allow ordinary user access to /dev/i2c-x)
  # pi@raspberrypi ~ $ cat /etc/udev/rules.d/88-i2c.rules
  # KERNEL=="i2c-[0-9]", GROUP="i2c", MODE="0666"
  ## Constants specific to this setup and using the Raspian Kernel I2C:
  # From i2c-dev.h
  I2C_slave = 0x0703
  # pi@raspberrypi ~ $ i2cdetect -y 1
  #     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
  # ...
  # 20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
  DevicePath = "/dev/i2c-1"
  I2C_address = 0x20
  # MCP23008
  Direction_register = 0x00
  GP_register = 0x09
  # LCD
  Backlight_on = 0x80

  def initialize
    @device = File.new(DevicePath, 'w')
    # set MCP23008 GP pins to output:
    set_dir(0x01)
    # 4 bit mode LCD initialization:
    # a) reset, sent 3 times,
    set_gp(0,0b0011)
    sleep(0.005)
    set_gp(0,0b0011)
    sleep(0.0001) 
    set_gp(0,0b0011)
    # b) 4 bit mode,
    set_gp(0,0b0010)
    # c) 2 line mode,
    cmd(0b00101000)
    # d) clear display,
    cmd(0b00000001)
    # e) display on (cursor off,)
    cmd(0b00001100)
    # f) add the degree glyph.
    set_cgram(1,"\x0e\x0a\x0e\0\0\0\0\0")
  end

  def send(i2c_reg,val)
    @device.ioctl(I2C_slave,I2C_address)
    @device.syswrite([i2c_reg,val].pack("CC"))
  end

  def set_dir(val)
    send(Direction_register,val)
  end

  def set_gp(rs,nibble)
    gp = Backlight_on + (nibble << 3) + (rs << 1)
    send(GP_register,gp)
    sleep(0.0001)
    send(GP_register,gp | 0x04)
    sleep(0.0001)
    send(GP_register,gp)
  end

  def cmd(c)
    set_gp(0,c >> 4)
    set_gp(0,c & 0x0f)
  end

  def print(s)
    s.each_byte do |c|
      set_gp(1,c >> 4)
      set_gp(1,c & 0x0f)
    end
  end

  def move_to(row,col)
    # For 4x20 LCD, the 0x80 character memory is in the order rows: 1, 3, 2, 4.
    row_starts = [0,64,20,84]
    r_offset = row_starts[row] || 0
    col = col >= 0 && col <= 19 ? col : 0
    ddram_address = col + r_offset
    cmd(0x80 | ddram_address)
  end

  def print_at(r,c,str)
    move_to(r,c)
    print(str)
  end

  def set_cgram(ord,pixels)
    ord = ord > 0 ? ord % 8 : 0
    cgram_address_start = ord << 3
    cmd(0x40 | cgram_address_start)
    print(pixels)
    clear # Get out of the CGRAM address space.
  end

  def clear
    cmd(0b00000001)
  end
end

# Plotting:
class StripChart
  def initialize(high_limit,low_limit)
    @high_limit = high_limit
    @low_limit = low_limit
    @bbox = [480 + 60 + 10,(@high_limit + 12) * 10 + 10]
  end

  def plot(points,initials)
    points = plottable_points(points)
    pts_def = "/pts [" +
              points.map {|p| "[#{p[0]} #{p[1]}]"}.join(' ') +
              "] def"
    initials = plottable_initials(initials)
    initials_def = "/initials [" +
                   initials.map {|p| "[#{p[0]} (#{p[1]})]"}.join(' ') +
                   "] def"
    open("chart.ps","w") do |f|
      f.puts "%!PS"
      f.puts "/highlimit %.1f def" % @high_limit
      f.puts "/lowlimit %.1f def" % @low_limit
      f.puts pts_def
      f.puts initials_def
      f.puts open(WDIR + "paper.ps").read
    end
    gs_cmd = "gs -dBATCH -dNOPAUSE -sDEVICE=pnggray -g%dx%d " +
             "-sOutputFile=" + WDIR + "chart.png " + WDIR + "chart.ps 2>&1"
    gs = `#{gs_cmd % @bbox}`.split("\n")[-1]
    png = open(WDIR + "chart.png","rb").read
    open(WDIR + "server.log","a") do |f|
      f.puts "Ghostscript => #{gs}"
      f.puts "PNG is #{png.size} bytes."
    end
    png
  end

  def plottable_initials(initials)
    # For display purposes show initials more than 30 tenths of an hour apart.
    i = initials.size - 1
    while i > 0
      initials.delete_at(i-1) if initials[i][0] - 30 < initials[i-1][0]
      i -= 1
    end
    initials
  end

  def plottable_points(points)
    # Clamp y to edges of paper.
    # Also remove duplicate points after rounding x.
    yh = @high_limit + 5.8
    yl = @low_limit - 5.8
    pts = points.map do |p|
      p[0] = p[0].round
      p[1] = p[1] > yh ? yh : p[1]
      p[1] = p[1] < yl ? yl : p[1]
      [p[0],p[1]]
    end.uniq
# Testing for unique, plottable points: 
#    puts "#{points.size} points."
#    puts "#{pts.size} plottable points."
#  Printed:
#  4167 points.
#  475 plottable points.
  end
end

# Class to extract an HTTP request from a socket.
class Request
  attr_reader :request_type, :target, :content_type, :content_length,
              :params, :summary
  def initialize(session)
    request = [session.gets]  # 1st line of the request.
    request << session.gets while request[-1] =~ /\S/ # Remainder of header.
    @request_type = extract(request[0],/(GET|POST) /)
    query_string = extract(request[0],/[^?]*\?(\S+)/)
    if @request_type
      @target = extract(request[0],/#{@request_type} \/(.+?)[? ]/)
    else 
      @target = nil
    end
    headers = request[1..-1].join
    @content_type = extract(headers,/Content-Type:\s+(\S+)/)
    @content_length = extract(headers,/Content-Length:\s+(\S+)/).to_i
    if @request_type == 'POST'
      body = session.read(@content_length)
    else
      body = nil
    end
    kv_pairs = [query_string,body].compact
    if kv_pairs.empty?
      @params = {}
    else
      @params = Hash[kv_pairs.join("&").split(/&/).map do |kv|
        kv.split(/=/).map do |e|
          e.tr('+',' ').gsub(/%[a-fA-F0-9][a-fA-F0-9]/){|c| c[1,2].hex.chr}
        end
      end]
    end
    @summary =  "#{@request_type} #{@target} received " + `date` +
                "Content-Length: #{@content_length}\n" +
                @params.keys.map {|k| "#{k}:#{@params[k]}\n"}.join
  end

  def extract(str,pattern)
    m = str.match(pattern)
    m && m[1]
  end

end
# Server response subroutines and templates:
HTML_WRAPPER = <<WRAP
<html><!-- template(title,heading,stats,page) -->
<head><title>Vaccine Storage Temperature <!-- title -->%s</title></head>
<body>
  <h2>Remote Vaccine Storage Temperature Monitor</h2>
  <!-- heading -->%s
  <p><!-- stats -->%s</p>
  <!-- page template -->%s
</body>
</html>
WRAP
INDEX_HTML = <<IDX
  <p align="right"><a href="admin.html" >Administrative page</a></p>
  <p><img src="chart.png" ></p>
  <p><form action="initial" method="POST">
       Initials: <input type="text" name="initials" value="" size="6" >
       <input type="submit" value="Initial the Log" >
       (Initialing will acknowledge and reset some warnings.)
  </form></p>
IDX
ADMIN_HEADING = "<h3>Administrative Functions</h3>\n"
ADMIN_HTML = <<ADMIN
  <!-- Admin user page(messages,log-content) -->
  <p align="right"><a href="index.html" >Home page</a></p>
  <p><form action="admin.html" method="POST">
      n: <input type="text" name="n" value="10" size="8" >
      <br>Log:
      <input type="radio" name="log" value="templog" checked > Temperature Log
      <input type="radio" name="log" value="bidlog" > B.I.D. Initials Log
      <input type="submit" name="adfunction" value="View Last n Log Records" >
      <br>Password: <input type="password" name="passwd" value="" size="8" >
      <input type="submit" name="adfunction" value="Rotate the Log Files" >
  </form></p>
  <p><!-- messages -->%s</p>
  <p><!-- log-content -->%s</p>
ADMIN

def stats(ds,log)
  [ds.time_stamp,ds.warnings,log.last_24h_summary].select {|s| s !~ /^\s*$/}
end

def respond(session,title,heading,status,body,mime="HTML")
  content_type = {"HTML" => "text/html","PNG" => "image/png"}
  if mime == "HTML"
    reply = HTML_WRAPPER % [title,heading,status.join("<br>"),body]
  elsif mime == "PNG"
    reply = body
  else
    reply = HTML_WRAPPER % ["error","","","#{mime} is not supported"]
  end
  begin
    session.puts "HTTP/1.1 200 OK"
    session.puts "Content-Type: #{content_type[mime]}"
    session.puts "Content-Length: #{reply.size}"
    session.puts
    session.print reply
  rescue Errno::EPIPE
    open(WDIR + "server.log","a") {|f| f.puts "Connection closed by client"}
  end
end

def redirect(session,get)
  session.puts "HTTP/1.1 303 See Other"
  session.puts "Location: http://192.168.1.72:#{PORT}/#{get}.html"
  session.puts
end

# Entry:
begin
# Read the globals from the SD card.
open("limits.txt") do |f|
  global_settings = f.gets.split
  $high_c = global_settings[0].to_f 
  $low_c = global_settings[1].to_f 
  $interval = global_settings[2].to_i
end
# Instantiate the monitored resources.
ds = DS18B20.new
log = TempLog.new(TEMPLOG)
bid = BidLog.new(BIDLOG)
ip_address = `ifconfig`.match(/wlan0.*?inet addr:([0-9.]{7,15})/m)[1]
# Load the "paper" for the strip chart,
# at startup now and anytime the limits are changed.
sc = StripChart.new($high_c,$low_c)
# Separate thread for the data-logger.
Thread.abort_on_exception = true
Thread.start do |datalog|
  loop do
    log.puts(ds.time_stamp)
    sleep($interval)
  end
end
server = TCPServer.new(PORT)
# Start the server log.
open(WDIR + "server.log","a") {|f| f.puts "Server started on #{ip_address}"}
ds.lcd.print_at(2,0,"View the log at:")
ds.lcd.print_at(3,0,ip_address + ":#{PORT}")
loop do
  Thread.start(server.accept) do |session|
    r = Request.new(session)
    open(WDIR + "server.log","a") {|f| f.puts r.summary}
    if r.request_type == "GET" and r.target =~ /chart\.png/i
      initials = bid.get_today_and_yesterday
      points = log.get_today_and_yesterday
      respond(session,"chart.png",nil,nil,sc.plot(points,initials),"PNG")
    elsif r.request_type == "GET" and (!r.target or r.target =~ /index\.html/i)
      respond(session,"index.html","",stats(ds,log),INDEX_HTML)
    elsif r.request_type == "GET" and r.target =~ /admin\.html/i
      respond(session,"admin.html",ADMIN_HEADING,stats(ds,log),
          ADMIN_HTML % ["Temperature Log last rotated: #{log.latest[1]}<br>" +
                        "B.I.D. Log last rotated: #{bid.latest[1]}",""])
    elsif r.request_type == "POST" and r.target =~ /initial/i
      initials = (r.params["initials"] || "").strip
      if initials.size >= 2
        bid.puts(initials,ds.severe_warnings)
        ds.clear_severe
      end
      redirect(session,'index')
    elsif r.request_type == "POST" and r.target =~ /admin\.html/i
      log_content = ""
      msg = ""
      adfunction = r.params["adfunction"] || ""
      logfile = (r.params["log"] || "templog") == "bidlog" ? bid : log
      n = r.params["n"].to_i
      if adfunction == "View Last n Log Records"
        log_content = logfile.tail(n).join("<br>")
      elsif adfunction == "Rotate the Log Files"
        phash = `grep 'admin' /etc/shadow`.split(":")[1]
        pw = (r.params["passwd"] || "").strip
        if pw.crypt(phash.match(/^\$[1-6]\$[^$]+/)[0]) == phash
          logfile.rotate
          rot_msg = "#{logfile == bid ? "B.I.D. Initials":"Temperature"} Log "
          msg += rot_msg + "was rotated.<br>"
          bid.puts("log rot",rot_msg + "rotated")
        else
          msg += "Error -- Please re-check your password!<br>"
        end
      end
      msg += "Temperature Log last rotated: #{log.latest[1]}<br>" +
            "B.I.D. Log last rotated: #{bid.latest[1]}"
      respond(session,"admin.html",ADMIN_HEADING,stats(ds,log),
          ADMIN_HTML % [msg,log_content])
    else
      respond(session,"not-supported.html","",[""],
              "Error: #{r.target} is not a supported function!")
    end
    sleep 0.01 # Delay for a slower client.
    session.close
  end
end
ensure
  # Turn off the Monitor On and In Range LED when exiting.
  green(false)
  # Mark the server log and close it.
  open(WDIR + "server.log","a") {|f| f.puts "Server shutdown"}
end
% paper.ps Fixed part of program -- the paper with the grid and labels:
/zero 0.0 def
/degc {10 mul} def
/xrange 480 def
/xpts {xrange 60 add} def
/yrange {highlimit degc} def
/ypts {yrange 120 add} def
/lheight 12 def
/Helvetica findfont lheight scalefont setfont
% Margins
5 5 translate
% Grid
0 10 xpts {
  dup 0 moveto
  ypts lineto
} for
0 10 ypts {
  dup 0 exch moveto
  xpts exch lineto
} for
0.75 setgray
stroke
0 setgray
% Reset 0,0
40 60 translate
/ytitle {highlimit 4 add degc} def
/yinitials {highlimit 2 add degc} def
/hcenter { % x y string
  dup stringwidth pop 4 -1 roll
  exch 2 div sub % y string x
  3 -1 roll moveto show} def
% Title
xrange 2 div ytitle
(Recent Vaccine Storage Temperature in Degrees Celsius) hcenter
% Initials label
-20 yinitials (Initials:) hcenter
% x axis tics and labels
/xlabels [(12am) (3am) (6am) (9am) (12pm) (3pm) (6pm) (9pm)] def
/xticlen [15 5 5 5 10 5 5 5] def
/yday -50 def
/yhour -30 def
% Yesterday
120 yday (Yesterday) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for 
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
stroke
% Today
gsave
240 0 translate
120 yday (Today) hcenter
0 1 7 {dup 30 mul yhour xlabels 4 -1 roll get hcenter} for
240 yhour xlabels 0 get hcenter
0 1 7 {dup 30 mul dup 0 moveto
       0 xticlen 4 -1 roll get sub lineto} for
240 0 moveto 240 0 xticlen 0 get sub lineto
stroke
grestore
% Draw high limit, low limit and 0 reference lines.
[highlimit lowlimit zero]
{dup 0 exch degc moveto
xrange exch degc lineto} forall
3 setlinewidth
stroke
1 setlinewidth
% Label the reference lines.
[highlimit lowlimit zero]
{dup 5 string cvs dup stringwidth pop 2 add neg
lheight 3 div 4 -1 roll degc exch sub
moveto show} forall
% Draw arrows beneath initials.
/iarrow { % x y
  2 sub moveto currentpoint 10 sub lineto
  currentpoint 2 copy exch 3 add exch 3 add lineto
  moveto currentpoint exch 3 sub exch 3 add lineto} def
% Plot the initials.
initials length 0 gt {
  initials {0 get yinitials iarrow}forall
  stroke
  initials {aload pop exch yinitials 3 -1 roll hcenter} forall }if
% Plot the points.
pts length 0 gt {
  % Pen down to plot contiguous points and pen up in gaps.
  -1                  % initial expected-x
  pts {dup            % save the point
    0 get             % put x on stack
    dup 1 add         % next-x
    exch 4 2 roll     % expected-x, pt, x, next-x -> next-x, x, expected-x, pt  
    aload pop degc    % x, y
    4 2 roll          % x, expected-x, x, y -> x, y, x, expected-x
    gt                % is x gt expected-x
      {moveto}        % pen up
      {lineto}ifelse  % pen down
    }forall
  stroke}if
showpage
LCD display

Week: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Revised August 18, 2013