Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for G&G JJ100B scale #7

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion trickler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def main(config, args, pidtune_logger):
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--auto_mode', action='store_true')
parser.add_argument('--pid_tune', action='store_true')
parser.add_argument('--target_weight', type=decimal.Decimal, default=0)
parser.add_argument('--target_weight', type=decimal.Decimal, default=10) # default=0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this change. You can keep it locally for testing if you like, but it should not go into the repo.

parser.add_argument('--target_unit', choices=scales.UNIT_MAP.keys(), default='GN')
args = parser.parse_args()

Expand Down
136 changes: 132 additions & 4 deletions trickler/scales.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ class Units(enum.Enum):

UNIT_MAP = {
'GN': Units.GRAINS,
'g': Units.GRAMS,
'G': Units.GRAMS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change and the one below would break support for AND scales if committed. The problem is that I designed it around AND scales and need to refactor this sort of thing to allow other brands with different data formats and unit names to be separated.

I'll handle the separation in this other PR (#5 ), which I'll need to merge first so that it will make it more clear what you'll need to do for your scale.

}


UNIT_REVERSE_MAP = {
Units.GRAINS: 'GN',
Units.GRAMS: 'g',
Units.GRAMS: 'G',
}


Expand All @@ -40,6 +40,133 @@ def noop(*args, **kwargs):
return


class GGjj100b:
"""Class for controlling a G&G JJ100B scale."""

class ScaleStatusV1(enum.Enum):
"""Supports the first version of OpenTrickler software."""
STABLE = 0
UNSTABLE = 1
OVERLOAD = 2
ERROR = 3
MODEL_NUMBER = 4
SERIAL_NUMBER = 5
ACKNOWLEDGE = 6

class ScaleStatusV2(enum.Enum):
"""New version avoids zero (0) which can be confused with null/None."""
STABLE = 1
UNSTABLE = 2
OVERLOAD = 3
ERROR = 4
MODEL_NUMBER = 5
SERIAL_NUMBER = 6
ACKNOWLEDGE = 7

def __init__(self, memcache, port='/dev/ttyUSB0', baudrate=9600, timeout=0.1, _version=1, **kwargs):
"""Controller."""
self._memcache = memcache
self._serial = serial.Serial(port=port, baudrate=baudrate, timeout=timeout, **kwargs)
# Set default values, which should be overwritten quickly.
self.raw = b''
self.unit = Units.GRAINS
self.resolution = decimal.Decimal(0.02)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this resolution correct for your scale?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negativ, resolution for grains = 0.01

self.weight = decimal.Decimal('0.00')

self.StatusMap = self.ScaleStatusV1
if _version == 2:
self.StatusMap = self.ScaleStatusV2

self.status = self.StatusMap.STABLE
self.model_number = None
self.serial_number = None
atexit.register(self._graceful_exit)

def _graceful_exit(self):
"""Graceful exit, closes serial port."""
logging.debug('Closing serial port...')
self._serial.close()

def change_unit(self):
"""Changes the unit of weight on the scale."""
logging.debug('changing weight unit on scale from: %r', self.unit)
# Send Mode button command.
self._serial.write(b'\x1B\x73')
erichiggins marked this conversation as resolved.
Show resolved Hide resolved
# Sleep 1s and wait for change to take effect.
time.sleep(1)
# Run update fn to set latest values.
self.update()

@property
def is_stable(self):
"""Returns True if the scale is stable, otherwise False."""
return self.status == self.StatusMap.STABLE
# returns 1 when previous weights match with current
# return len(set(self._previous_reads)) == 1

def update(self):
"""Read from the serial port and update an instance of this class with the most recent values."""
handlers = {
' GN': self._stable,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should other units (such as grams, ounces, etc) be listed here as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the trickler project it is not necessary but my scale supports following units so the missing units could be implemented here:
grams (g), grains (GN), ounces (oz), carat (ct), troy ounce(ozt), pennyweight (dwt), pound (lb)

' ': self._unstable,
'F---.-H': self._overload,
None: noop,
}

# Note: The input buffer can fill up, causing latency. Clear it before reading.
self._serial.reset_input_buffer()
self._serial.write(b'\x1B\x70')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need to send a command before reading from the scale, please document what is being sent and why it's needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to get a reading from the scale the hardware number (default 27dec = 0x1B) + 0x70 has to be send to the scale

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Have you tested this with your scale to see if it works? Is a delay needed after sending before the weight can be read?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have tested this with my scale. I put a delay for safety reasons but I will test if it works without delay too. Here is a sample code I used for reading and changing units on my scale:

import atexit
import serial
import time

ser = serial.Serial(port='/dev/ttyUSB0', baudrate=9600, timeout=0.1)
atexit.register(ser.close)

def change_units():
ser.write(b'\x1B\x73')

def read_units():
ser.write(b'\x1B\x70')
units = ser.readline().decode('utf-8')
units = units[9:12]
print(units)
return units

if name == "main":

while read_units() != " GN":
    change_units()
    time.sleep(1)

raw = self._serial.readline()
self.raw = raw
logging.debug(raw)
try:
line = raw.strip().decode('utf-8')
except UnicodeDecodeError:
logging.debug('Could not decode bytes to unicode.')
else:
status = line[9:12].strip()
fault = line[2:9].strip()
handler = handlers.get(status, noop)
handler2 = handlers.get(fault, noop)
handler(line)
handler2(line)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may be a better way to do this than calling both functions when only one is needed. I know that it's a little more complicated because of the data format. I don't have a suggestion off the top of my head.

At the very least, let's rename the functions to something more clear instead of handler and handler2 and add some comments to document what is being done here and why.


def _stable_unstable(self, line):
"""Update the scale when status is stable or unstable."""
weight = line[2:9].strip()
self.weight = decimal.Decimal(weight)

unit = line[9:12].strip()
self.unit = UNIT_MAP[unit]

resolution = {}
resolution[Units.GRAINS] = decimal.Decimal(0.02)
resolution[Units.GRAMS] = decimal.Decimal(0.001)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these resolutions correct for your scale?


self.resolution = resolution[self.unit]
# Update memcache values.
self._memcache.set(constants.SCALE_STATUS, self.status)
self._memcache.set(constants.SCALE_WEIGHT, self.weight)
self._memcache.set(constants.SCALE_UNIT, self.unit)
self._memcache.set(constants.SCALE_RESOLUTION, self.resolution)
self._memcache.set(constants.SCALE_IS_STABLE, self.is_stable)

def _stable(self, line):
"""Scale is stable."""
self.status = self.StatusMap.STABLE
self._stable_unstable(line)

def _unstable(self, line):
"""Scale is unstable."""
self.status = self.StatusMap.UNSTABLE
self._stable_unstable(line)

def _overload(self, line):
"""Scale is overloaded."""
self.status = self.StatusMap.OVERLOAD
self._memcache.set(constants.SCALE_STATUS, self.status)


class ANDFx120:
"""Class for controlling an A&D FX120 scale."""

Expand Down Expand Up @@ -187,6 +314,7 @@ def _serial_number(self, line):

SCALES = {
'and-fx120': ANDFx120,
'jj100b': GGjj100b,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please match the naming convention and change the key to 'gg-jj100b'

}


Expand All @@ -196,9 +324,9 @@ def _serial_number(self, line):
import helpers

parser = argparse.ArgumentParser(description='Test scale.')
parser.add_argument('--scale', choices=SCALES.keys(), default='and-fx120')
parser.add_argument('--scale', choices=SCALES.keys(), default='jj100b')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this.

parser.add_argument('--scale_port', default='/dev/ttyUSB0')
parser.add_argument('--scale_baudrate', type=int, default='19200')
parser.add_argument('--scale_baudrate', type=int, default='9600')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this.

parser.add_argument('--scale_timeout', type=float, default='0.1')
parser.add_argument('--scale_version', type=int, default='1')
args = parser.parse_args()
Expand Down