Skip to content

Commit 8f59447

Browse files
committed
ads1x1x: auto calculate sample rate and better error handling
Signed-off-by: Konstantin Koch <[email protected]>
1 parent 3105a5d commit 8f59447

File tree

2 files changed

+125
-111
lines changed

2 files changed

+125
-111
lines changed

docs/Config_Reference.md

+9-13
Original file line numberDiff line numberDiff line change
@@ -4830,28 +4830,24 @@ ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 are I2C based Analog to
48304830
Digital Converters that can be used for temperature sensors. They provide 4
48314831
analog input pins either as single line or as differential input.
48324832

4833+
Note: Use caution if using this sensor to control heaters. The heater min_temp
4834+
and max_temp are only verified in the host and only if the host is running and
4835+
operating normally. (ADC inputs directly connected to the micro-controller
4836+
verify min_temp and max_temp within the micro-controller and do not require a
4837+
working connection to the host.)
4838+
48334839
```
48344840
[ads1x1x my_ads1x1x]
48354841
chip: ADS1115
4836-
#pga: 6.144V
4842+
#pga: 4.096V
48374843
# Default value is 4.096V. The maximum voltage range used for the input. This
48384844
# scales all values read from the ADC. Options are: 6.144V, 4.096V, 2.048V,
48394845
# 1.024V, 0.512V, 0.256V
4840-
#mode: single
4841-
# Default value is single. Turn off the chip after a single reading or keep
4842-
# it running. Turning off saves power but turning it back on will be slower.
4843-
# Options are single and continuous.
4844-
#samples_per_second: 128
4845-
# Default value is 128. The amount of samples that the ADC can provide per
4846-
# second. A lower value makes the samples more accurate, but it takes longer
4847-
# until a new value is available.
4848-
# ADS101X's support 128, 250, 490, 920, 1600, 2400, 3300.
4849-
# ADS111X's support 8, 16, 32, 64, 128, 250, 475, 860.
48504846
i2c_mcu: host
48514847
i2c_bus: i2c.1
48524848
#address_pin: GND
48534849
# Default value is GND. There can be up to four addressed devices depending
4854-
# upon wiring of the device. Check the datasheet for details. The i2_address
4850+
# upon wiring of the device. Check the datasheet for details. The i2c_address
48554851
# can be specified directly instead of using the address_pin.
48564852
```
48574853

@@ -4861,7 +4857,7 @@ The chip provides pins that can be used on other sensors.
48614857
sensor_type: ...
48624858
# Can be any thermistor or adc_temperature.
48634859
sensor_pin: my_ads1x1x:AIN0
4864-
# A combination of the name of the ads1x1x chip that and the pin. Possible
4860+
# A combination of the name of the ads1x1x chip and the pin. Possible
48654861
# pin values are AIN0, AIN1, AIN2 and AIN3 for single ended lines and
48664862
# DIFF01, DIFF03, DIFF13 and DIFF23 for differential between their
48674863
# correspoding lines. For example

klippy/extras/ads1x1x.py

+116-98
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import logging
77
import pins
88
from . import bus
9-
from . import adc_temperature
109

1110
# Supported chip types
1211
ADS1X1X_CHIP_TYPE = {
@@ -173,22 +172,10 @@ def __init__(self, config):
173172
if config.get('address_pin', None) is not None:
174173
address = config.getchoice('address_pin', ADS1X1X_CHIP_ADDR)
175174

176-
if isADS101X(self.chip):
177-
self.samples_per_second = config.getchoice('samples_per_second',
178-
ADS101X_SAMPLES_PER_SECOND, '128')
179-
self.samples_per_second_numeric = config.getint(
180-
'samples_per_second', 128)
181-
else:
182-
self.samples_per_second = config.getchoice('samples_per_second',
183-
ADS111X_SAMPLES_PER_SECOND, '128')
184-
self.samples_per_second_numeric = config.getint(
185-
'samples_per_second', 128)
186-
187175
self._ppins = self._printer.lookup_object("pins")
188176
self._ppins.register_chip(self.name, self)
189177

190178
self.pga = config.getchoice('pga', ADS1X1X_PGA, '4.096V')
191-
self.mode = config.getchoice('mode', ADS1X1X_MODE, 'single')
192179
# Comparators are not implemented, they would only be useful if the
193180
# alert pin is used, which we haven't made configurable.
194181
# But that wouldn't be useful for a normal temperature sensor anyway.
@@ -201,55 +188,112 @@ def __init__(self, config):
201188
self.mcu = self._i2c.get_mcu()
202189

203190
self._printer.add_object("ads1x1x " + self.name, self)
204-
self._printer.register_event_handler("klippy:ready", \
205-
self._handle_ready)
206-
self._printer.register_event_handler("klippy:shutdown", \
207-
self.reset_all_devices)
191+
self._printer.register_event_handler("klippy:connect", \
192+
self._handle_connect)
208193

209194
self._pins = {}
210195
self._mutex = self._reactor.mutex()
211196

212197
def setup_pin(self, pin_type, pin_params):
198+
pin = pin_params['pin']
213199
if pin_type == 'adc':
214-
pin = ADS1X1X_pin(self, pin_params)
215-
if pin.pin in self._pins:
200+
if (pin not in ADS1X1X_MUX):
201+
raise pins.error('ADS1x1x pin %s is not valid' % \
202+
pin_params['pin'])
203+
204+
config = 0
205+
config |= (ADS1X1X_OS['OS_SINGLE'] & \
206+
ADS1X1X_REG_CONFIG['OS_MASK'])
207+
config |= (ADS1X1X_MUX[pin_params['pin']] & \
208+
ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK'])
209+
config |= (self.pga & ADS1X1X_REG_CONFIG['PGA_MASK'])
210+
# Have to use single mode, because in continuous, it never reaches
211+
# idle state, which we use to determine if the sampling is done.
212+
config |= (ADS1X1X_MODE['single'] & \
213+
ADS1X1X_REG_CONFIG['MODE_MASK'])
214+
# lowest sample rate per default, until report time has been set in
215+
# setup_adc_sample
216+
config |= (self.comp_mode \
217+
& ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK'])
218+
config |= (self.comp_polarity \
219+
& ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK'])
220+
config |= (self.comp_latching \
221+
& ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK'])
222+
config |= (self.comp_queue \
223+
& ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK'])
224+
225+
pin_obj = ADS1X1X_pin(self, config)
226+
if pin in self._pins:
216227
raise pins.error(
217-
'%s pin %s for chip %s is used multiple times' \
218-
% (self.chip, pin.pin, self.name))
219-
self._pins[pin.pin] = pin
220-
return pin
228+
'pin %s for chip %s is used multiple times' \
229+
% (pin, self.name))
230+
self._pins[pin] = pin_obj
231+
232+
return pin_obj
221233
raise pins.error('Wrong pin or incompatible type: %s with type %s! ' % (
222-
pin_params['pin'], pin_type))
234+
pin, pin_type))
223235

224-
def _handle_ready(self):
225-
self.reset_all_devices()
236+
def _handle_connect(self):
237+
try:
238+
# Init all devices on bus for this kind of device
239+
self._i2c.i2c_write([0x06, 0x00, 0x00])
240+
except Exception:
241+
logging.exception("ADS1X1X: error while resetting device")
226242

227243
def is_ready(self):
228244
config = self._read_register(ADS1X1X_REG_POINTER['CONFIG'])
229245
return bool((config & ADS1X1X_REG_CONFIG['OS_MASK']) == \
230246
ADS1X1X_OS['OS_IDLE'])
231247

232-
def sample(self, sensor):
248+
def calculate_sample_rate(self):
249+
pin_count = len(self._pins)
250+
lowest_report_time = 1
251+
for pin in self._pins.values():
252+
lowest_report_time = min(lowest_report_time, pin.report_time)
253+
254+
sample_rate = 1 / lowest_report_time * pin_count
255+
samples_per_second = ADS111X_SAMPLES_PER_SECOND
256+
if isADS101X(self.chip):
257+
samples_per_second = ADS101X_SAMPLES_PER_SECOND
258+
259+
# make sure the samples list is sorted correctly by number.
260+
samples_per_second = sorted(samples_per_second.items(), \
261+
key=lambda t: int(t[0]))
262+
for rate, bits in samples_per_second:
263+
rate_number = int(rate)
264+
if sample_rate <= rate_number:
265+
return (rate_number, bits)
266+
logging.warning(
267+
"ADS1X1X: requested sample rate %s is higher than supported by %s."\
268+
% (sample_rate, self.name))
269+
return (rate_number, bits)
270+
271+
def handle_report_time_update(self):
272+
(sample_rate, sample_rate_bits) = self.calculate_sample_rate()
273+
274+
for pin in self._pins.values():
275+
pin.config = (pin.config & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \
276+
| (sample_rate_bits & ADS1X1X_REG_CONFIG['DATA_RATE_MASK'])
277+
278+
self.delay = 1 / float(sample_rate)
279+
280+
def sample(self, pin):
233281
with self._mutex:
234-
pin_object = self._pins[sensor.pin]
235-
sample = 0
236282
try:
237-
self._write_register(ADS1X1X_REG_POINTER['CONFIG'],
238-
pin_object.config)
239-
# The report time is 1 / sample_count * 4 to account for the 4
240-
# possible inputs. So sample_count 16 on 1 input will result
241-
# in 4 samples per second.
242-
delay = 1 / self.samples_per_second_numeric
243-
self._reactor.pause(self._reactor.monotonic() + delay)
283+
self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.config)
284+
self._reactor.pause(self._reactor.monotonic() + self.delay)
285+
start_time = self._reactor.monotonic()
244286
while not self.is_ready():
245287
self._reactor.pause(self._reactor.monotonic() + 0.001)
246-
sample = self._read_register(ADS1X1X_REG_POINTER['CONVERSION'])
247-
except Exception:
248-
logging.exception("ADS1X1X: error while sampling")
288+
# if we waited twice the expected time, mark this an error
289+
if start_time + self.delay < self._reactor.monotonic():
290+
logging.warning("ADS1X1X: timeout during sampling")
291+
return None
292+
return self._read_register(ADS1X1X_REG_POINTER['CONVERSION'])
293+
except Exception as e:
294+
logging.exception("ADS1X1X: error while sampling: %s" % str(e))
249295
return None
250296

251-
return sample
252-
253297
def _read_register(self, reg):
254298
# read a single register
255299
params = self._i2c.i2c_read([reg], 2)
@@ -264,89 +308,63 @@ def _write_register(self, reg, data):
264308
]
265309
self._i2c.i2c_write(data)
266310

267-
def reset_all_devices(self):
268-
try:
269-
# Init all devices on bus for this kind of device
270-
self._i2c.i2c_write([0x06, 0x00, 0x00])
271-
except Exception:
272-
logging.exception("ADS1X1X: error while resetting device")
273-
274311
class ADS1X1X_pin:
275-
def __init__(self, chip, pin_params):
312+
def __init__(self, chip, config):
276313
self.mcu = chip.mcu
277314
self.chip = chip
278-
self.pin = pin_params['pin']
279-
280-
if (self.pin not in ADS1X1X_MUX):
281-
raise pins.error('ADS1x1x pin %s is not valid' % self.pin)
282-
283-
self.mux = ADS1X1X_MUX[self.pin]
284-
# Set up 2-byte configuration that will be used with each request
285-
self.config = 0
286-
self.config |= (ADS1X1X_OS['OS_SINGLE'] \
287-
& ADS1X1X_REG_CONFIG['OS_MASK'])
288-
self.config |= (self.mux & ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK'])
289-
self.config |= (chip.pga & ADS1X1X_REG_CONFIG['PGA_MASK'])
290-
self.config |= (chip.mode & ADS1X1X_REG_CONFIG['MODE_MASK'])
291-
self.config |= (chip.samples_per_second & \
292-
ADS1X1X_REG_CONFIG['DATA_RATE_MASK'])
293-
self.config |= (chip.comp_mode \
294-
& ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK'])
295-
self.config |= (chip.comp_polarity \
296-
& ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK'])
297-
self.config |= (chip.comp_latching \
298-
& ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK'])
299-
self.config |= (chip.comp_queue \
300-
& ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK'])
301-
302-
self.report_time = 1.0 / chip.samples_per_second_numeric * 4
315+
self.config = config
316+
303317
self.invalid_count = 0
304318

305319
self.chip._printer.register_event_handler("klippy:connect", \
306-
self._handle_connect)
320+
self._handle_connect)
307321

308322
def _handle_connect(self):
309323
self._reactor = self.chip._printer.get_reactor()
310324
self._sample_timer = \
311-
self._reactor.register_timer(self._timer)
312-
self._reactor.update_timer(self._sample_timer, self._reactor.NOW)
325+
self._reactor.register_timer(self._process_sample, \
326+
self._reactor.NOW)
313327

314-
def _timer(self, eventtime):
328+
def _process_sample(self, eventtime):
315329
sample = self.chip.sample(self)
316330
if sample is not None:
317-
self._process_sample(sample)
318-
319-
return self._reactor.monotonic() + self.report_time
320-
321-
def _process_sample(self, sample):
322-
# The sample is encoded in the top 12 or full 16 bits
323-
# Value's meaning is defined by ADS1X1X_REG_CONFIG['PGA_MASK']
324-
if isADS101X(self.chip.chip):
325-
sample >>= 4
326-
target_value = sample / ADS101X_RESOLUTION
327-
else:
328-
target_value = sample / ADS111X_RESOLUTION
331+
# The sample is encoded in the top 12 or full 16 bits
332+
# Value's meaning is defined by ADS1X1X_REG_CONFIG['PGA_MASK']
333+
if isADS101X(self.chip.chip):
334+
sample >>= 4
335+
target_value = sample / ADS101X_RESOLUTION
336+
else:
337+
target_value = sample / ADS111X_RESOLUTION
329338

330-
if self.maxval > self.minval:
331339
if target_value > self.maxval or target_value < self.minval:
332340
self.invalid_count = self.invalid_count + 1
333-
if self.invalid_count > self.range_check_count:
334-
self.chip._printer.invoke_shutdown(
335-
"ADS1X1X temperature outside range")
341+
logging.warning("ADS1X1X: temperature outside range")
342+
self.check_invalid()
336343
else:
337344
self.invalid_count = 0
338345

339-
# Publish result
340-
measured_time = self._reactor.monotonic()
341-
self.callback(self.chip.mcu.estimated_print_time(measured_time),
342-
target_value)
346+
# Publish result
347+
measured_time = self._reactor.monotonic()
348+
self.callback(self.chip.mcu.estimated_print_time(measured_time),
349+
target_value)
350+
else:
351+
self.invalid_count = self.invalid_count + 1
352+
self.check_invalid()
353+
354+
return eventtime + self.report_time
355+
356+
def check_invalid(self):
357+
if self.invalid_count > self.range_check_count:
358+
self.chip._printer.invoke_shutdown(
359+
"ADS1X1X temperature check failed")
343360

344361
def get_mcu(self):
345362
return self.mcu
346363

347364
def setup_adc_callback(self, report_time, callback):
348365
self.report_time = report_time
349366
self.callback = callback
367+
self.chip.handle_report_time_update()
350368

351369
def setup_adc_sample(self, sample_time, sample_count,
352370
minval=0., maxval=1., range_check_count=0):

0 commit comments

Comments
 (0)