Updated 2016-11-20 11:03:42 by chw

After my Android tablet provided good service cutting up onions an immediate need arose to weigh the ingredients for the onion tart. With a HX711 load cell amplifier, a load cell, an Arduino, and AndroWish the tablet is even more useful.

Here is the Arduino program
#include "HX711.h"
#include <EEPROM.h>
#include <avr/wdt.h>

// HX711.DOUT       - pin 3 (digital)
// HX711.PD_SCK     - pin 2 (digital)

HX711 scale(3, 2);  // parameter "gain" left out
                    // the default value 128 is used by the library

int run;            // when true, measure in main loop
int nrd;            // number measurements/actions (1..9) to average
int inChar;         // char from serial line
byte inByte;        // byte from/to EEPROM
long lVal;          // used e.g. for R/W of scale value to EEPROM
double dVal;        // used for calibration

void setup() {
  run = 0;
  nrd = 5;
  Serial.begin(9600);
  lVal = 0;
  EEPROM.get(0, inByte);
  lVal |= inByte;
  EEPROM.get(1, inByte);
  lVal <<= 8;
  lVal |= inByte;
  EEPROM.get(2, inByte);
  lVal <<= 8;
  lVal |= inByte;
  EEPROM.get(3, inByte);
  lVal <<= 8;
  lVal |= inByte;
  scale.set_scale(lVal);      // use -680000 for 3 kg load cell
  scale.tare(nrd);
  scale.power_down();
}

void wrcal(long val) {
  byte b;
  b = val >> 24;
  EEPROM.update(0, b);
  b = val >> 16;
  EEPROM.update(1, b);
  b = val >> 8;
  EEPROM.update(2, b);
  b = val;
  EEPROM.update(3, b);
} 

void loop() {
  if (Serial.available() > 0) {
    inChar = Serial.read();
    if (inChar == -1) {
      return;
    }
    switch (inChar) {
      case 'S':               // STOP 
      case 's':
        if (run) {
          scale.power_down();
        }
        run = 0;
        break;
      case '?':               // PRINT CALIBRATION SCALE
        Serial.print('C');
        lVal = scale.get_scale();
        Serial.println(lVal);
        break;
      case 'C':               // SET CALIBRATION SCALE
      case 'c':
        lVal = Serial.parseInt();
        if (lVal) {
          scale.set_scale(lVal);
          wrcal(lVal);
        }
        Serial.print('C');
        lVal = scale.get_scale();
        Serial.println(lVal);
        break;
      case 'T':             // TARE
      case 't':
        if (!run) {
          scale.power_up();
        }
        scale.tare(nrd);
        if (!run) {
          scale.power_down();
        }
        break;
      case 'V':             // MEASURE, GIVE RAW VALUE
      case 'v':
        if (!run) {
          scale.power_up();
        }
        lVal = scale.read_average(nrd) - scale.get_offset();
        if (!run) {
          scale.power_down();
        }
        Serial.print('V');
        Serial.println(lVal);
        break;
      case 'M':             // MEASURE, GIVE GRAMS
      case 'm':
        if (!run) {
          scale.power_up();
          Serial.println(scale.get_units(nrd) * 1000.0, 1);
          scale.power_down();
        }
        break;
      case 'R':             // RUN
      case 'r':
        if (!run) {
          scale.power_up();
        }
        run = 1;
        break;
      case 'X':             // REBOOT
      case 'x':
        wdt_enable(WDTO_15MS);
        while (1) {
        }
        break;
      case 'Y':             // DO CALIBRATION GIVEN GRAMS
      case 'y':
        dVal = Serial.parseFloat() / 1000.0;
        if (dVal) {
          if (!run) {
            scale.power_up();
          }
          lVal = scale.read_average(nrd) - scale.get_offset();
          if (!run) {
            scale.power_down();
          }
          scale.set_scale(lVal / dVal);
          lVal = scale.get_scale();
          wrcal(lVal);
          Serial.print('C');
          Serial.println(lVal);
        }
        break;
      default:              // # MEASUREMENTS FOR AVERAGING
        if (inChar >= '1' && inChar <= '9') {
          nrd = inChar - '0';
        }
        break;
    }
  }
  if (run) {
    Serial.println(scale.get_units(nrd) * 1000.0, 1);
  }
}

And this is the Tcl script for AndroWish which can be run with undroidwish, too.
# AndroWish script to interface Arduino with HX711 scale amplifier and load cell
#
# Wiring:
#
#  +--------------+       +-------------+   +-----------+   +------+
#  | Android      |       |          +5V|---| HX711   E+|---|      |
#  | Device       |--USB--| Arduino  GND|---|         E-|---| Load |
#  | w/ AndroWish |       |           D3|---|DOUT     A-|---| Cell |
#  +--------------+       |           D2|---|PC_SCK   A+|---|      |
#                         +-------------+   +-----------+   +------+

wm attributes . -fullscreen 1
catch {borg screenorientation landscape}
sdltk screensaver 0

option add *Radiobutton.borderWidth 3
option add *Radiobutton.highlightThickness 0
option add *Button.borderWidth 3
option add *Button.highlightThickness 0

# Open/reopen serial line to Arduino
proc reopen {} {
    after cancel reopen
    if {[sdltk android]} {
        if {[catch {
            set dev [lindex [usbserial] 0]
            set ::SCALE(chan) [usbserial $dev]
            fconfigure $::SCALE(chan) -blocking 0 -translation binary \
                -mode 9600,n,8,1
            fileevent $::SCALE(chan) readable readv
            set ::SCALE(value) "online"
            puts -nonewline $::SCALE(chan) "S"
        } err]} {
            sdltk log verbose $err
            set ::SCALE(value) "offline"
            after 1000 reopen
            .run configure -background [lindex [.run configure -background] 3]
            return
        }
    } else {
        if {[catch {
            set ::SCALE(chan) [open "/dev/ttyUSB0" {RDWR NONBLOCK}]
            fconfigure $::SCALE(chan) -translation binary -mode 9600,n,8,1
            fileevent $::SCALE(chan) readable readv
            set ::SCALE(value) "online"
        } err]} {
            sdltk log verbose $err
            set ::SCALE(value) "offline"
            after 1000 reopen
            .run configure -background [lindex [.run configure -background] 3]
            return
        }
    }
    sendc 5 S
}

# Send command string to Arduino
proc sendc args {
    foreach cmd $args {
        if {[catch {
            puts -nonewline $::SCALE(chan) $cmd
            flush $::SCALE(chan)
        } err]} {
            sdltk log verbose $err
            catch {close $::SCALE(chan)}
            set ::SCALE(value) "offline"
            after 1000 reopen
            watchdog -1
            return
        } elseif {$cmd eq "R"} {
            watchdog 1
        } elseif {$cmd eq "S"} {
            watchdog -1
        } elseif {$cmd eq "T"} {
            set ::SCALE(value) [format "%7.1fg" 0]
        } elseif {[string match {[1-9]} $cmd]} {
            set ::SCALE(avg) $cmd
        }
    }
}

# Monitor continuous measurement
proc watchdog {{on 0}} {
    after cancel watchdog 
    if {$on > 0} {
        if {[.run cget -background] ne "green3"} {
            .run configure -background green3 \
                -activebackground green3
        }
        after 2000 watchdog
    } else {
        .run configure -background [lindex [.run configure -background] 3] \
            -activebackground [lindex [.run configure -activebackground] 3]
    }
}

# Read data from Arduino
proc readv {} {
    if {[eof $::SCALE(chan)]} {
        sdltk log verbose "EOF detected"
        catch {close $::SCALE(chan)}
        set ::SCALE(value) "offline"
        after 1000 reopen
        watchdog -1
        return
    }
    if {[catch {gets $::SCALE(chan) v} err]} {
        sdltk log verbose $err
        catch {close $::SCALE(chan)}
        set ::SCALE(value) "offline"
        after 1000 reopen
        watchdog -1
        return
    }
    if {($v ne "") && [scan $v %g vv]} {
        set ::SCALE(value) [format "%7.1fg" $vv]
        after cancel watchdog 
        after 2000 watchdog
    }
}

# Stop Arduino and exit
proc stop_and_exit {} {
    sendc S
    exit
}

# Start calibration on Arduino
proc docalib {} {
    sendc "Y$::SCALE(calg)\r"
    set ::SCALE(caldone) 1
}

# Cancel calibration
proc calcancel {} {
    set ::SCALE(caldone) -1
}

# Calibration dialog
proc calib {} {
    toplevel .calib
    wm title .calib "Calibration"
    wm transient .calib .
    wm protocol .calib WM_DELETE_WINDOW calcancel
    label .calib.l1 -text "1.  Remove everything from the load cell." \
        -font {Helvetica -25} -anchor w
    label .calib.l2 -text "2.  Tare the load cell by pressing this button.  " \
        -font {Helvetica -25} -anchor w
    button .calib.tare -text "Tare" -command {sendc T} -font {Helvetica -30} \
        -width 8
    label .calib.l3 -text "3.  Place a known weight on the load cell." \
        -font {Helvetica -25} -anchor w
    label .calib.l4 -text "4.  Enter that weight in grams here:  " \
        -font {Helvetica -25} -anchor w
    entry .calib.grams -width 10 -textvariable ::SCALE(calg) -font {Courier -30}
    label .calib.l5 -text "5.  Perform calibration by pressing this button.  " \
        -font {Helvetica -25} -anchor w
    button .calib.cal -text "Calibrate" -command docalib -font {Helvetica -30} \
        -width 8
    grid .calib.l1 -row 0 -column 0 -pady 20 -sticky w -padx 10
    grid .calib.l2 -row 1 -column 0 -pady 20 -sticky w -padx 10
    grid .calib.tare -row 1 -column 1 -padx 20
    grid .calib.l3 -row 2 -column 0 -pady 20 -sticky w -padx 10
    grid .calib.l4 -row 3 -column 0 -pady 20 -sticky w -padx 10
    grid .calib.grams -row 3 -column 1 -padx 20
    grid .calib.l5 -row 4 -column 0 -pady 20 -sticky w -padx 10
    grid .calib.cal -row 4 -column 1 -padx 20
    bind .calib <Escape> {calcancel ; break}
    bind .calib <Key-q> {calcancel ; break}
    bind .calib <Key-Q> {calcancel ; break}
    ::tk::PlaceWindow .calib
    ::tk::SetFocusGrab .calib .calib
    sendc S 9
    vwait ::SCALE(caldone)
    ::tk::RestoreFocusGrab .calib .calib destroy
}

# Main screen
label .weight -textvariable SCALE(value) -font {Courier -160}
grid .weight -row 0 -column 0 -columnspan 9 -sticky news

button .tare -text "Tare" -command {sendc T} -font {Helvetica -30}
button .run -text "Run" -command {sendc R R}  -font {Helvetica -30}
button .stop -text "Stop" -command {sendc S} -font {Helvetica -30}
button .single -text "Meas." -command {sendc M} -font {Helvetica -30}
grid .tare .run .stop .single -row 1 -rowspan 2 -sticky news

foreach i {1 2 3 4 5} {
    radiobutton .m$i -text $i -command [list sendc $i] -indicatoron 0 \
        -value $i -variable ::SCALE(avg) -font {Helvetica -30}
    grid .m$i -row 1 -column [expr {$i + 3}] -sticky news
}

foreach i {6 7 8 9} {
    radiobutton .m$i -text $i -command [list sendc $i] -indicatoron 0 \
        -value $i -variable ::SCALE(avg) -font {Helvetica -30}
    grid .m$i -row 2 -column [expr {$i - 2}] -sticky news
}

button .cal -text "Cal." -command calib -font {Helvetica -30}
grid .cal -row 2 -column 8 -sticky news

grid columnconfigure . 0 -uniform largebutton
grid columnconfigure . 1 -uniform largebutton
grid columnconfigure . 2 -uniform largebutton
grid columnconfigure . 3 -uniform largebutton

grid columnconfigure . 4 -uniform smallbutton
grid columnconfigure . 5 -uniform smallbutton
grid columnconfigure . 6 -uniform smallbutton
grid columnconfigure . 7 -uniform smallbutton
grid columnconfigure . 8 -uniform smallbutton

grid rowconfigure . 0 -weight 12
grid rowconfigure . 1 -weight 1 -uniform button
grid rowconfigure . 2 -weight 1 -uniform button

grid columnconfigure . all -weight 1

# Keyboard shortcuts to buttons etc.
bind all <Key-0> {.tare invoke}
bind all <Key-t> {.tare invoke}
bind all <Key-T> {.tare invoke}
bind all <Key-r> {.run invoke}
bind all <Key-R> {.run invoke}
bind all <Key-s> {.stop invoke}
bind all <Key-S> {.stop invoke}
bind all <Key-m> {.single invoke}
bind all <Key-M> {.single invoke}
foreach i {1 2 3 4 5 6 7 8 9} {
    bind all <Key-$i> [list .m$i invoke]
}
bind all <Key-q> stop_and_exit
bind all <Key-Q> stop_and_exit
bind all <Escape> stop_and_exit
bind all <Break> stop_and_exit

reopen