Skip to content

Commit 086a9cc

Browse files
authored
Initial version
1 parent fcf3459 commit 086a9cc

2 files changed

Lines changed: 350 additions & 0 deletions

File tree

example.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/python3
2+
3+
"""
4+
Examples of how to use the TSIC 206/306 temperature reading class
5+
on a Raspberry PI.
6+
"""
7+
8+
__author__ = 'Holger Fleischmann'
9+
__copyright__ = 'Copyright 2018, Holger Fleischmann, Bavaria/Germany'
10+
__license__ = 'Apache License 2.0'
11+
12+
import pigpio
13+
from tsic import TsicInputChannel
14+
import time
15+
16+
# TsicInputChannel and ZacWireInputChannel require pigpio
17+
# for GPIO access with precise timing:
18+
pi = pigpio.pi()
19+
20+
tsic = TsicInputChannel(pigpio_pi=pi, gpio=17)
21+
22+
print('\nA. Single measurement:')
23+
print(str(tsic.measure_once(timeout=1.0)))
24+
25+
print('\nB. All measurements for 1 second:')
26+
tsic.start(lambda measurement: print(measurement))
27+
time.sleep(1)
28+
tsic.stop()
29+
30+
print('\nC. One measurement per second for 3 seconds:')
31+
32+
# start receiving in a context:
33+
with tsic:
34+
for i in range(3):
35+
time.sleep(1)
36+
print('{:d} {:.1f}°C'.format(i+1, tsic.measurement.degree_celsius))
37+
38+
pi.stop()

tsic.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
#! /usr/bin/python3
2+
3+
"""
4+
TSIC temperature sensor and ZACWire protocol support for Raspberry PI.
5+
May be used as a command line tool for testing TSIC input.
6+
"""
7+
8+
__all__ = [ 'ZacWireInputChannel', 'Measurement', 'TsicInputChannel' ]
9+
10+
__author__ = 'Holger Fleischmann'
11+
__copyright__ = 'Copyright 2018, Holger Fleischmann, Bavaria/Germany'
12+
__license__ = 'Apache License 2.0'
13+
14+
from datetime import datetime
15+
import argparse
16+
17+
import pigpio
18+
import threading
19+
import time
20+
21+
22+
class ZacWireInputChannel(object):
23+
"""
24+
ZACWire protocol GPIO packet receiving handler. Receive packets
25+
of bytes consisting of 8 bits plus an even parity bit.
26+
"""
27+
28+
STATUS_OK = 0
29+
""" Received data is valid. """
30+
STATUS_PARITY_ERROR = 1
31+
""" Received data has a parity error. """
32+
STATUS_BIT_COUNT_ERROR = 2
33+
""" Received data has a wrong bit count. """
34+
35+
def __init__(self, pigpio_pi, gpio):
36+
"""
37+
Initialize ZACWire receiving channel on a GPIO.
38+
pigpio_pi is the pigpio.pi object to use for GPIO access.
39+
gpio is the GPIO as Broadcom chip number.
40+
"""
41+
self.pi = pigpio_pi
42+
self.gpio = gpio
43+
self.__pi_callback = None
44+
45+
self.pi.set_mode(self.gpio, pigpio.INPUT)
46+
47+
def start(self, callback):
48+
"""
49+
Start listening for data and pass received packet bytes
50+
to the a callback callback(status, [bytes]).
51+
Note that the callback is called by a pigpio thread.
52+
"""
53+
self.stop()
54+
self.packet_callback = callback
55+
self.__pi_callback = self.pi.callback(
56+
self.gpio,
57+
pigpio.EITHER_EDGE,
58+
lambda gpio, level, tick: self.__gpio_callback(gpio, level, tick))
59+
60+
def stop(self):
61+
"""
62+
Stop listening for data.
63+
"""
64+
if not self.__pi_callback is None:
65+
self.pi.set_watchdog(self.gpio, 0)
66+
self.__pi_callback.cancel()
67+
self.__pi_callback = None
68+
self.__reset_packet()
69+
self.__last_low_tick = None
70+
self.__last_high_tick = None
71+
72+
def is_started(self):
73+
"""
74+
Whether listening for data is running.
75+
"""
76+
return not self.__pi_callback is None
77+
78+
def __enter__(self):
79+
self.start(None)
80+
81+
def __exit__(self, exc_type, exc_val, exc_tb):
82+
self.stop()
83+
84+
def __call_packet_callback(self, status, packet_bytes):
85+
self.packet_callback(status, packet_bytes)
86+
87+
def __reset_packet(self):
88+
self.__bit_ticks = None
89+
self.__parity = 0
90+
self.__bit_count = 0
91+
self.__received_bytes = None
92+
93+
def __pass_any_packet_to_callback(self, status):
94+
if not self.__received_bytes is None:
95+
# print('====> RECEIVED {0} STATUS {1}'.format(self.__received_bytes, status))
96+
self.__call_packet_callback(status, self.__received_bytes)
97+
self.__received_bytes = None
98+
99+
def __gpio_callback(self, gpio, level, tick):
100+
if level == pigpio.LOW:
101+
102+
if not self.__last_high_tick is None:
103+
high_ticks = pigpio.tickDiff(self.__last_high_tick, tick)
104+
105+
if high_ticks > 1000:
106+
# packet start
107+
# print('====> PACKET START')
108+
self.__pass_any_packet_to_callback(self.STATUS_OK)
109+
self.__received_bytes = [0]
110+
self.__parity = 0
111+
self.__bit_count = 0
112+
self.__bit_ticks = None
113+
114+
elif not self.__received_bytes is None and high_ticks > 150:
115+
# next byte in packet
116+
# print('====> NEXT BYTE START')
117+
if self.__bit_count == 9:
118+
self.__received_bytes.append(0)
119+
self.__bit_ticks = None
120+
self.__bit_count = 0
121+
self.__parity = 0
122+
else:
123+
self.__pass_any_packet_to_callback(self.STATUS_BIT_COUNT_ERROR)
124+
self.__reset_packet()
125+
126+
self.__last_low_tick = tick
127+
128+
elif level == pigpio.HIGH:
129+
130+
if not self.__last_low_tick is None:
131+
low_ticks = pigpio.tickDiff(self.__last_low_tick, tick)
132+
# print('{0} @ {1} -> {2}: {3}'.format(gpio, tick, level, low_ticks))
133+
134+
if not self.__received_bytes is None:
135+
if self.__bit_ticks is None:
136+
# calibration T-strobe at begin of byte
137+
self.__bit_ticks = low_ticks
138+
else:
139+
# a 0-bit has a short high interval, 1-bit a long high interval
140+
bit = 0 if low_ticks > self.__bit_ticks else 1
141+
if self.__bit_count < 8:
142+
# data bit received
143+
self.__received_bytes[-1] = self.__received_bytes[-1] * 2 + bit
144+
self.__bit_count += 1
145+
146+
self.__parity += bit
147+
if self.__bit_count == 9:
148+
self.__check_parity()
149+
self.pi.set_watchdog(self.gpio, 1) # 1 ms
150+
elif self.__bit_count > 9:
151+
# more bits than expected (8 bits + 1 __parity)
152+
self.__pass_any_packet_to_callback(self.STATUS_BIT_COUNT_ERROR)
153+
self.__reset_packet()
154+
155+
self.__last_high_tick = tick
156+
157+
elif level == pigpio.TIMEOUT:
158+
self.__pass_any_packet_to_callback(self.STATUS_OK)
159+
self.__reset_packet()
160+
161+
def __check_parity(self):
162+
if self.__parity % 2 != 0:
163+
self.__pass_any_packet_to_callback(self.STATUS_PARITY_ERROR)
164+
self.__reset_packet()
165+
166+
167+
class Measurement(object):
168+
"""
169+
Measurement consisting of the temperature degree_celsius and the timestamp
170+
seconds_since_epoch.
171+
"""
172+
173+
def __init__(self, degree_celsius, seconds_since_epoch):
174+
self.degree_celsius = degree_celsius
175+
self.seconds_since_epoch = seconds_since_epoch
176+
177+
def __repr__(self, *args, **kwargs):
178+
if self.degree_celsius is None:
179+
return 'Undefined'
180+
else:
181+
return (self.__class__.__name__
182+
+ ' {:.2f}°C at {}'
183+
.format(self.degree_celsius,
184+
datetime.fromtimestamp(self.seconds_since_epoch).isoformat(sep=' ')))
185+
186+
187+
Measurement.UNDEF = Measurement(None, None)
188+
""" Undefined measurement """
189+
190+
191+
class TsicInputChannel(object):
192+
"""
193+
Receive temperature measurements from a TSIC 206 or TSIC 306 sensor
194+
connected to a Raspberry PI GPIO channel.
195+
"""
196+
197+
def __init__(self, pigpio_pi, gpio):
198+
"""
199+
Initialize TSIC receiving channel.
200+
pigpio_pi is the pigpio.pi object to use for GPIO access.
201+
gpio is the as GPIO Broadcom chip number.
202+
"""
203+
self.__callback = None
204+
self.__degree_celsius = None
205+
self.__timestamp = None
206+
self.__zacwire_channel = ZacWireInputChannel(pigpio_pi, gpio)
207+
self.__lock = threading.RLock()
208+
self.__measure_waiting = threading.Condition()
209+
210+
def start(self, callback=None):
211+
"""
212+
Start reading temperatures from the TSIC. Optionally pass each
213+
successfully received measurement to a callback callback(Measurement)
214+
if callback is not None.
215+
216+
Note that the callback is called by a pigpio thread.
217+
218+
You can also fetch the last reading from property measurement.
219+
"""
220+
self.stop()
221+
self.__callback = callback
222+
self.__zacwire_channel.start(lambda status, packet_bytes: self.__packet_received(status, packet_bytes))
223+
224+
def stop(self):
225+
"""
226+
Stop reading temperatures.
227+
"""
228+
if not self.__zacwire_channel is None:
229+
self.__zacwire_channel.stop()
230+
231+
def is_started(self):
232+
"""
233+
Whether reading temperatures is currently running.
234+
"""
235+
return self.__zacwire_channel.is_started()
236+
237+
def __enter__(self):
238+
self.start(None)
239+
240+
def __exit__(self, exc_type, exc_val, exc_tb):
241+
self.stop()
242+
243+
def measure_once(self, timeout=None):
244+
"""
245+
Wait up to optional timeout seconds for a measurement and
246+
return the first successfully received measurement as
247+
Measurement or Measurement.UNDEF otherwise.
248+
Temporarily start and stop measurement if it is not yet running.
249+
"""
250+
last_timestamp = self.__timestamp
251+
was_started = self.is_started()
252+
if not was_started:
253+
self.start()
254+
255+
with self.__measure_waiting:
256+
self.__measure_waiting.wait(timeout)
257+
258+
if not was_started:
259+
self.stop()
260+
261+
measurement = self.measurement
262+
if last_timestamp != measurement.seconds_since_epoch:
263+
return measurement
264+
else:
265+
return Measurement.UNDEF
266+
267+
@property
268+
def measurement(self):
269+
"""
270+
The last received measurement as Measurement.
271+
"""
272+
with self.__lock:
273+
return Measurement(self.__degree_celsius, self.__timestamp)
274+
275+
def __packet_received(self, status, packet_bytes):
276+
if status == ZacWireInputChannel.STATUS_OK and len(packet_bytes) == 2:
277+
278+
with self.__lock:
279+
self.__degree_celsius = ((packet_bytes[0] * 256 + packet_bytes[1]) / 2047. * (150 + 50) - 50)
280+
self.__timestamp = time.time()
281+
measurement = self.measurement
282+
283+
# print('====> Temperature {0}°C'.format(self.__degree_celsius))
284+
if not self.__callback is None:
285+
self.__callback(measurement)
286+
287+
with self.__measure_waiting:
288+
self.__measure_waiting.notifyAll()
289+
290+
291+
if __name__ == '__main__':
292+
parser = argparse.ArgumentParser(description=
293+
'''Read temperatures from a TSIC 206/306 sensor
294+
connected to a Raspberry PI GPIO pin.''')
295+
parser.add_argument('gpio', type=int, help='GPIO pin as Broadcom number')
296+
parser.add_argument('--loop', dest='loop', action='store_const', const=True, default=False, help='print each received measurement until break')
297+
args = parser.parse_args()
298+
299+
pi = pigpio.pi()
300+
tsic = TsicInputChannel(pi, args.gpio)
301+
try:
302+
if args.loop:
303+
tsic.start(callback=lambda m: print(m))
304+
# wait forever:
305+
threading.Semaphore(0).acquire()
306+
else:
307+
print(tsic.measure_once(timeout=1.0))
308+
except KeyboardInterrupt:
309+
pass
310+
finally:
311+
pi.stop()
312+

0 commit comments

Comments
 (0)