Commit 02a86c97bb58037f3cd4c1b4771a7dcf1adbd391
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
.gitignore
View file @
02a86c9
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 |