Commit 02a86c97bb58037f3cd4c1b4771a7dcf1adbd391

Authored by Greg Sandstrom
1 parent 802a2f1110
Exists in main

copy and tweak tc08 monitor code from ssa project

Showing 5 changed files with 363 additions and 0 deletions Inline Diff

File was created 1 *.pyc
2 .vscode/
influxdb_bridge.py View file @ 02a86c9
File was created 1 import datetime
2 import influxdb
3
4
5 class InfluxDBBridge:
6 def __init__(self):
7 # todo make this stuff configurable
8 self.host = "localhost"
9 self.port = 8086
10 self.username = None
11 self.password = None
12 self.dbname = "logger"
13
14 def connect(self):
15 # create and open client
16 self.client = influxdb.InfluxDBClient(
17 self.host, self.port, self.username, self.password, self.dbname
18 )
19 # try to create db just in case
20 try:
21 self.client.create_database(self.dbname)
22 except influxdb.client.InfluxDBClientError:
23 # db already exists, proceed
tc08_device.py View file @ 02a86c9
File was created 1 # inspired by example at
2 # https://github.com/picotech/picosdk-python-wrappers/blob/master/usbtc08Examples/tc08StreamingModeExample.py
3 #
4 # API docs
5 # https://www.picotech.com/download/manuals/usb-tc08-thermocouple-data-logger-programmers-guide.pdf
6
7 import ctypes
8 import signal
9 import time
10 from threading import Timer
11 from picosdk.usbtc08 import usbtc08 as tc08
12 from picosdk.usbtc08 import USBTC08_INFO
13
14 DEBUG = False
15
16
17 def dprint(arg):
18 if DEBUG:
19 print(arg)
20
21
22 ERROR_CODES = [
23 "USBTC08_ERROR_OK",
24 "USBTC08_ERROR_OS_NOT_SUPPORTED",
25 "USBTC08_ERROR_NO_CHANNELS_SET",
26 "USBTC08_ERROR_INVALID_PARAMETER",
27 "USBTC08_ERROR_VARIANT_NOT_SUPPORTED",
28 "USBTC08_ERROR_INCORRECT_MODE",
29 "USBTC08_ERROR_ENUMERATION_INCOMPLETE",
30 "USBTC08_ERROR_NOT_RESPONDING",
31 "USBTC08_ERROR_FW_FAIL",
32 "USBTC08_ERROR_CONFIG_FAIL",
33 "USBTC08_ERROR_NOT_FOUND",
34 "USBTC08_ERROR_THREAD_FAIL",
35 "USBTC08_ERROR_PIPE_INFO_FAIL",
36 "USBTC08_ERROR_NOT_CALIBRATED",
37 "USBTC08_ERROR_PICOPP_TOO_OLD",
38 "USBTC08_ERROR_COMMUNICATION",
39 ]
40
41
42 class TC08:
43 def __init__(self, handle):
44 self.handle = handle
45 # populate info about self
46 self.info = USBTC08_INFO()
47 # must set size attribute before any call to get_unit_info
48 self.info.size = ctypes.sizeof(USBTC08_INFO)
49 rc = tc08.usb_tc08_get_unit_info(self.handle, ctypes.byref(self.info))
50 TC08.check_return(self.handle, rc)
51
52 def __str__(self):
53 return (
54 f"TC08 Oject\n"
55 f"\tHandle: {self.handle}\n"
56 f"\tSerial Number: {self.get_serial()}"
57 )
58
59 def get_serial(self):
60 return getattr(self.info, "szSerial[USBTC08_MAX_SERIAL_CHAR]").decode()
61
62 # static method to open and return a new TC08
63 @staticmethod
64 def get_any_tc08():
65 open_status = tc08.usb_tc08_open_unit()
66 if open_status > 0:
67 # successfully opened a tc08 unit, create a TC08 object using that handle
68 handle = open_status
69 dprint(f"Found device handle {handle}")
70 device = TC08(handle)
71 return device
72 elif open_status == 0:
73 # no more units found, return a None object
74 return None
75 else:
76 # error occured with the open_unit call, try to grab an error code
77 TC08.check_return(0, open_status)
78
79 # helper to check return codes
80 @staticmethod
81 def check_return(handle, rc):
82 if rc <= 0:
83 dprint(f"last return code: {rc}")
84 # tc08 functions seem to return 0 or -1 on failure, in which case
85 # we can querry get_last_error for a more detailed error code
86 error_code = tc08.usb_tc08_get_last_error(handle)
87 if error_code == -1:
88 raise Exception(
89 f"(handle {handle}) Error retrieving last error (oh the irony) (last return code: {rc})"
90 )
91 if error_code == 0:
92 # error_code of 0 from usb_tc08_get_last_error is USBTC08_ERROR_OK - "No error occurred"
93 return
94 if error_code >= 0 and error_code < len(ERROR_CODES):
95 raise Exception(
96 f"(handle {handle}) Error {error_code}: {ERROR_CODES[error_code]}"
97 )
98 else:
99 raise Exception(f"(handle {handle}) Unknown error code: {error_code}")
100
101 # non-static version
102 def check(self, rc):
103 return TC08.check_return(self.handle, rc)
104
105 # must be called before any get_data calls
106 def setup(self):
107 # set mains rejection to 60hz
108 rc = tc08.usb_tc08_set_mains(self.handle, 1)
109 self.check(rc)
110
111 # set up cold junction channel 0
112 rc = tc08.usb_tc08_set_channel(self.handle, 0, ord("C"))
113 self.check(rc)
114 # set up specified channels as typeK
115 for channel in range(1, 9):
116 rc = tc08.usb_tc08_set_channel(self.handle, channel, ord("K"))
117 self.check(rc)
118
119 # Invoke single get_data call, use time.time() to get approximate timestamp
120 # Returns tuple of ([channel_0, channel_1, ..., channel_9], timestamp_ms, overflow)
121 def get_single_data(self):
122 timestamp_sec = time.time()
123 timestamp_ms = int(timestamp_sec * 1000)
124 temperature_data = (ctypes.c_float * 9)()
125 overflow = ctypes.c_int16(0)
126 rc = tc08.usb_tc08_get_single(
127 self.handle,
128 ctypes.byref(temperature_data),
129 ctypes.byref(overflow),
130 tc08.USBTC08_UNITS["USBTC08_UNITS_CENTIGRADE"],
131 )
132 self.check(rc)
133 data = []
134 for i in range(0, 9):
135 data.append(temperature_data[i])
136 return (data, timestamp_ms)
137
138 def close(self):
139 rc = tc08.usb_tc08_close_unit(self.handle)
140 self.check(rc)
141
142 #
143 # functions for using streaming mode
144 #
145
146 def run(self, sampling_interval_ms):
147 # get minimum sampling interval in ms as sanity check
148 rc = tc08.usb_tc08_get_minimum_interval_ms(self.handle)
149 self.check(rc)
150 min_sampling_interval_ms = rc
151
152 interval_ms = max(sampling_interval_ms, min_sampling_interval_ms)
153 rc = tc08.usb_tc08_run(self.handle, interval_ms)
154 self.check(rc)
155 self.start_time_sec = time.time()
156
157 def stop(self):
158 rc = tc08.usb_tc08_stop(self.handle)
159 self.check(rc)
160
161 # tc08 timestamps readings with ms since usb_tc08_run called
162 def convert_time(self, relative_time_ms):
163 return int((self.start_time_sec * 1000) + relative_time_ms)
164
165 # Return list of lists of readings for configured channels specified in setup
166 # Each element of array is a (temperature, timestamp, overflow) tuple
167 # e.g.
168 # [
169 # 0: [(channel0_temp1, time1, o1), (channel0_temp2, time2, o2)],
170 # 1: [(channel1_temp1, time1, o1), (channel1_temp2, time2, o2)],
171 # ...
172 # 9: [(channel9_temp1, time1, o1), (channel9_temp2, time2, o2)]
173 # ]
174 def get_streaming_data(self):
175 data = []
176 buffer_size = 8
177 temperature_buffer = (ctypes.c_float * buffer_size)()
178 times_ms_buffer = (ctypes.c_int32 * buffer_size)()
179 overflow = ctypes.c_int16()
180 for channel in range(0, 9):
181 # load in up to buffer_size readings from current channel
182 num_readings = tc08.usb_tc08_get_temp(
183 self.handle,
temperature_logger.py View file @ 02a86c9
File was created 1 # inspired by example at
2 # https://github.com/picotech/picosdk-python-wrappers/blob/master/usbtc08Examples/tc08StreamingModeExample.py
3 #
4 # API docs
5 # https://www.picotech.com/download/manuals/usb-tc08-thermocouple-data-logger-programmers-guide.pdf
6
7 import signal
8 from threading import Timer
9 from tc08_device import TC08
10 from influxdb_bridge import InfluxDBBridge
11
12 # how often does Monitor check for new data from the TC08 driver
13 LOG_INTERVAL_SEC = 5
14
15 DEBUG = False
16
17
18 def dprint(arg):
19 if DEBUG:
20 print(arg)
21
22
23 class Monitor(Timer):
24 def __init__(self, interval):
25 # Timer constructer expects a callback function, but we don't care
26 # about that, so just pass None
27 super(Monitor, self).__init__(interval, None)
28 self.idb = None
29 self.tc08s = list()
30
31 # override Timer's run method to loop on our injest_data function
32 # wait will block while loop for timeout interval
33 # calling timer.cancel() sets the finish flag and exits the loop
34 def run(self):
35 dprint("in timer.run method")
36 while not self.finished.wait(timeout=self.interval):
37 self.do_work()
38
39 # connect as many tc08's as we can find
40 def setup(self):
41 searching = True
42 while searching:
43 device = TC08.get_any_tc08()
44 if device is None:
45 dprint(f"No more TC08s to dicsover")
46 searching = False
47 else:
48 dprint(f"Found TC08: {device}")
49 self.tc08s.append(device)
50
51 # setup and run
52 for device in self.tc08s:
53 device.setup()
54 device.run(LOG_INTERVAL_SEC * 1000)
55
56 # open connection to influxdb
57 self.idb = InfluxDBBridge()
58 self.idb.connect()
59
60 dprint("end setup")
61
62 def cleaup(self):
63 for device in self.tc08s:
64 device.stop()
65 device.close()
66
67 def do_work(self):
68 points = []
69 for device in self.tc08s:
70 serial = device.get_serial()
71 data = device.get_streaming_data()
72 for channel in range(0, 9):
73 for reading in data[channel]:
74 (temperature, time_ms, overflow) = reading
75 point = self.make_point(
76 channel,
77 temperature,
78 time_ms,
79 overflow,
80 )
81 points.append(point)
82 self.idb.commit(points)
83
84 def make_point(self, channel, temperature, time_ms, overflow):
85 json_body = {
86 "measurement": "temperature",
87 "tags": {"channel": channel},
88 "time": time_ms,
89 "fields": {"temperature": temperature, "overflow": overflow},
90 }
91 return json_body
92
temperature_logger.service View file @ 02a86c9
File was created 1 [Unit]
2 Description=TC08 Temperature monitoring service
3 Requires=time-sync.target
4
5 [Service]
6 Type=simple
7 User=pi
8 PIDFile=/path/to/repo/tc08-run.pid
9 ExecStart=python -m temperature_logger
10 WorkingDirectory=/path/to/repo/
11 Restart=always
12 RestartSec=5
13 StandardOutput=journal
14 StandardError=journal
15