force___flex_faux_scale_display.002.png
Clue Coffee Scale Display Design Concept

Let's take a walk through the code and look in more detail how each section works within code.py.

The main module, code.py, prepares and operates the Coffee Scale. It consists of the following major sections:

  • Import and Set Defaults
  • Calibration Ratio
  • Instantiate the Sensor and Display
  • Display the Background Bitmap Image
  • Define and Display the Text Labels and Graphic Elements
  • Helpers
  • Activate the Sensor and Prepare to Loop
  • The Primary Code Loop

Import and Set Defaults

This section imports all the needed modules and libraries, including the NAU7802 sensor driver. After importing, the first visible task of this section is to turn the Clue NeoPixel to the color yellow to indicate that the Coffee Scale is initializing. During operation, the indicator LED will glow green when operating normally or red when zeroing the scale.

Next, the scale defaults are specified. These are constants with names that are capitalized to help identify them as constants. These are values that you can change to alter the operation of the scale.

MAX_GR is the full-scale value of the scale in grams. The tick mark values adjacent to the graduated scale graphic are automatically derived from MAX_GR which can by any positive integer value. Since the graduated scale graphic is divided into tenths of full-scale with five tick marks, it's best to choose an integer MAX_GR value that will display nicely such as 1000, 500, 250, 100, 50, 25, 10, or 5.

DEFAULT_GAIN is the gain setting of the NAU7802's internal ADC pre-amplifier. Normally, this is set to the highest value of 128.

SAMPLE_AVG specifies the number of measurements that are averaged when the load cell is measured. The measurement value will be more stable with a high SAMPLE_AVG value. The display will update more slowly with a high value; it's a trade-off between stability and speed.

SCALE_NAME_1 is the text string that appears on the display to the left of the graduated scale. This should be no longer than 6 characters. Upper case letters will look the best.

SCALE_NAME_2 is the text string that appears on the display to the right of the graduated scale. This should be no longer than 6 characters. Upper case letters will look the best on the display.

# import clue_scale_calibrator  # Uncomment to run calibrator method

import time
import board
from simpleio import map_range
from adafruit_clue import clue
from adafruit_display_shapes.circle import Circle
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
import displayio
from cedargrove_nau7802 import NAU7802

clue.pixel.brightness = 0.2  # Set NeoPixel brightness
clue.pixel[0] = clue.YELLOW  # Set status indicator to yellow (initializing)

# Set Scale Defaults
MAX_GR = 100  # Maximum (full-scale) display range in grams
DEFAULT_GAIN = 128  # Default gain for internal PGA
SAMPLE_AVG = 5  # Number of sample values to average
SCALE_NAME_1 = "COFFEE"  # 6 characters maximum
SCALE_NAME_2 = "SCALE"  # 6 characters maximum

Calibration Ratio

CALIB_RATIO is the factor that is used to convert the NAU7802's raw measurement value into grams. This ratio is updated after running the calibrator method. See the guide section, Calibrate the Load Cell for instructions. You should only have to measure and record the load cell calibration ratio once.

"""Enter the calibration ratio for the individual load cell in-use. The ratio is
composed of the reference weight in grams divided by the raw reading. For
example, a raw reading of 215300 for a 100 gram weight results in a calibration
ratio of 100 / 215300. Use the clue_scale_single_calibrate method to obtain the
raw value.
FYI: A US dime coin weighs 2.268 grams or 0.079 ounces."""
CALIB_RATIO = 100 / 215300  # load cell serial#4540-02

Instantiate the Sensor and Display

The NAU7802 Stemma board is connected to the Clue's I2C bus and lives at address 42 (hexadecimal 2A). The NAU7802 24-bit ADC (analog to digital converter) chip is a dual-channel device, but only the first channel is available on the Stemma board; active_channels is set to 1.

Next, the Clue's integrated display is instantiated along with the primary displayio graphics group layer, scale_group which will contain the background bitmap image and other group layers.

The three font objects for display titles, labels, and measurements are listed as constants that will be used when display labels are defined.

# Instantiate the Sensor and Display
nau7802 = NAU7802(board.I2C(), address=0x2A, active_channels=1)

display = board.DISPLAY
scale_group = displayio.Group()

FONT_0 = bitmap_font.load_font("/fonts/Helvetica-Bold-24.bdf")
FONT_1 = bitmap_font.load_font("/fonts/OpenSans-16.bdf")
FONT_2 = bitmap_font.load_font("/fonts/OpenSans-9.bdf")

Display the Background Bitmap Image

The display background containing the graduated scale is loaded from the Clue root directory and is appended as the first item in the primary displayio group layer. All other graphics objects such as labels and the indicator bubble will be put on layers above the background.

# Display the Background Bitmap Image
bkg = displayio.OnDiskBitmap("/clue_scale_bkg.bmp")
_background = displayio.TileGrid(bkg, pixel_shader=bkg.pixel_shader, x=0, y=0)
scale_group.append(_background)

Define and Display the Text Labels and Graphic Elements

Text Label and Graphic Element Layers in Action

This code section defines the graphic elements that will be placed on the display in front of the background graphic. First, the scale name text and zeroing button graphics are appended to the primary displayio group.

The subsection that starts with for i in range... is the code that steps through the graduated scale's tick marks, creating a value label calculated using the MAX_GR constant and appending them to the displayio group. Measurement values and units labels added next.

Lastly, the displayio indicator_group and it's floating indicator bubble is created and added to the primary displayio group. The indicator_group becomes the front-most graphics layer of the display.

The indicator bubble is a yellow circle that travels up and down the graduated scale, pointing to the measured value. The center of the circle will normally be transparent, but will appear yellow or red depending on the scale's current status; yellow when initializing, red when zeroing.

# Define and Display the Text Labels and Graphic Elements
# Place the project name on either side of the graduated scale
scale_name_1 = Label(FONT_1, text=SCALE_NAME_1, color=clue.CYAN)
scale_name_1.anchor_point = (0.5, 0.5)
scale_name_1.anchored_position = (40, 96)
scale_group.append(scale_name_1)

scale_name_2 = Label(FONT_1, text=SCALE_NAME_2, color=clue.CYAN)
scale_name_2.anchor_point = (0.5, 0.5)
scale_name_2.anchored_position = (199, 96)
scale_group.append(scale_name_2)

# Define the zeroing button graphic
zero_button_circle = Circle(14, 152, 14, fill=None, outline=clue.RED, stroke=2)
scale_group.append(zero_button_circle)

zero_button_label = Label(FONT_1, text="Z", color=clue.RED)
zero_button_label.x = 8
zero_button_label.y = 150
scale_group.append(zero_button_label)

# Place tickmark labels next to the graduated scale
for i in range(-1, 6):
    tick_value = Label(FONT_2, text=str((MAX_GR) // 5 * i), color=clue.CYAN)
    if i == -1:
        tick_value.anchor_point = (1.0, 1.1)
    elif i == 5:
        tick_value.anchor_point = (1.0, 0.0)
    else:
        tick_value.anchor_point = (1.0, 0.5)
    tick_value.anchored_position = (99, 201 - (i * 40))
    scale_group.append(tick_value)

# Place the grams and ounces labels and values near the bottom of the display
grams_label = Label(FONT_0, text="grams", color=clue.BLUE)
grams_label.anchor_point = (1.0, 0)
grams_label.anchored_position = (80, 216)
scale_group.append(grams_label)

ounces_label = Label(FONT_0, text="ounces", color=clue.BLUE)
ounces_label.anchor_point = (1.0, 0)
ounces_label.anchored_position = (230, 216)
scale_group.append(ounces_label)

grams_value = Label(FONT_0, text="0.0", color=clue.WHITE)
grams_value.anchor_point = (1.0, 0.5)
grams_value.anchored_position = (80, 200)
scale_group.append(grams_value)

ounces_value = Label(FONT_0, text="0.00", color=clue.WHITE)
ounces_value.anchor_point = (1.0, 0.5)
ounces_value.anchored_position = (230, 200)
scale_group.append(ounces_value)

# Define the moveable indicator bubble
indicator_group = displayio.Group()
bubble = Circle(120, 200, 10, fill=clue.YELLOW, outline=clue.YELLOW, stroke=3)
indicator_group.append(bubble)

scale_group.append(indicator_group)
display.root_group = scale_group

Helpers

These two helper functions work with the NAU7802 driver class to zero the scale and to read the load cell's current raw value.

The zero_channel( ) helper sets up and zeros the NAU7802's internal amplifiers and ADC (analog to digital converter) to prepare it to receive signals. This function is used when the scale is first powered-up as well as when manually zeroed by pressing the Clue's button A. 

The load cell's raw measurement value is obtained by the read( ) helper. This helper accepts an integer parameter that specifies the number of samples to be averaged each time the helper is called, defaulting to 1 sample (no averaging). The helper returns the averaged raw measurement value. Since the NAU7802's internal ADC has 24-bit accuracy, the raw value can range from about -8.3M to + 8.3M.

# Helpers
def zero_channel():
    """Prepare internal amplifier settings and zero the current channel. Use
    after power-up, a new channel is selected, or to adjust for measurement 
    drift. Can be used to zero the scale with a tare weight.
    The nau7802.calibrate function used here does not calibrate the load cell, 
    but sets the NAU7802 internals to prepare for measuring input signals."""
    nau7802.calibrate("INTERNAL")
    nau7802.calibrate("OFFSET")


def read(samples=1):
    """Read and average consecutive raw samples; return averaged value."""
    sample_sum = 0
    sample_count = samples
    while sample_count > 0:
        if nau7802.available():
            sample_sum = sample_sum + nau7802.read()
            sample_count -= 1
    return int(sample_sum / samples)

Activate the Sensor and Prepare to Loop

This is where the NAU7802 ADC is enabled and calibrated for use. Before calibrating and zeroing, the internal sensor amplifier's gain is set to the default value. Once completed, the Clue will chirp some welcoming notes.

# Activate the Sensor
# Enable the internal analog circuitry, set gain, and calibrate/zero
nau7802.enable(True)
nau7802.gain = DEFAULT_GAIN
zero_channel()

# Play "welcome" tones
clue.play_tone(1660, 0.15)
clue.play_tone(1440, 0.15)

The Primary Code Loop

The primary loop is the operational process of the scale. The loop indicates the scale's status on the Clue NeoPixel; green when the scale is operating, red when it's busy zeroing.

After setting the NeoPixel to green, the load cell raw value is measured and converted to grams and ounces. The converted values are formatted and placed into the corresponding display labels and is printed in the REPL.

The grams measurement value is used to position the on-screen indicator bubble along the graduated scale graphic using the map_range( ) function. Also, if the grams measurement value falls outside of the minimum or maximum range, the bubble is "parked" at the extreme position and its interior color is changed from transparent to red.

Finally, the Clue A button is watched to see if it has been pressed. When pressed, the Clue will play a sound and will begin zeroing the scale. During the zeroing process, the NeoPixel and the center of the bubble are set to a red color. When zeroing is completed, the code will wait until the button is released before playing a completion sound, setting the bubble interior to transparent, and returning to normal operation.

# The Primary Code Loop
# Read sensor, move bubble, and display values
while True:
    clue.pixel[0] = clue.GREEN  # Set status indicator to green (ready)

    # Read the raw scale value and scale for grams and ounces
    value = read(SAMPLE_AVG)
    mass_grams = round(value * CALIB_RATIO, 1)
    mass_ounces = round(mass_grams * 0.03527, 2)
    grams_value.text = f"{mass_grams:5.1f}"
    ounces_value.text = f"{mass_ounces:5.2f}"
    print(f" {mass_grams:5.1f} grams   {mass_ounces:5.2f} ounces")

    # Reposition the indicator bubble based on grams value
    min_gr = (MAX_GR // 5) * -1  # Minimum display value
    bubble.y = int(map_range(mass_grams, min_gr, MAX_GR, 240, 0)) - 10
    if mass_grams > MAX_GR or mass_grams < min_gr:
        bubble.fill = clue.RED
    else:
        bubble.fill = None

    # Check to see if the zeroing button is pressed
    if clue.button_a:
        # Zero the sensor
        clue.pixel[0] = clue.RED  # Set status indicator to red (stopped)
        bubble.fill = clue.RED  # Set bubble center to red (stopped)
        clue.play_tone(1660, 0.3)  # Play "button pressed" tone

        zero_channel()

        while clue.button_a:
            # Wait until the button is released
            time.sleep(0.1)

        clue.play_tone(1440, 0.5)  # Play "reset completed" tone
        bubble.fill = None  # Set bubble center to transparent (ready)

This guide was first published on Aug 17, 2022. It was last updated on Jul 14, 2024.

This page (Code Details) was last updated on Mar 08, 2024.

Text editor powered by tinymce.