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 Side-by-side Diff

  1 +*.pyc
  2 +.vscode/
influxdb_bridge.py View file @ 02a86c9
  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
  24 + pass
  25 +
  26 + def commit(self, json_body):
  27 + self.client.write_points(json_body, time_precision="ms")
tc08_device.py View file @ 02a86c9
  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,
  184 + ctypes.byref(temperature_buffer),
  185 + ctypes.byref(times_ms_buffer),
  186 + buffer_size,
  187 + ctypes.byref(overflow),
  188 + channel,
  189 + tc08.USBTC08_UNITS["USBTC08_UNITS_CENTIGRADE"],
  190 + 0,
  191 + )
  192 + # if num_readings is -1, there was an error
  193 + if num_readings < 0:
  194 + self.check(num_readings)
  195 + elif num_readings == 0:
  196 + # no data, nothing to do
  197 + dprint("no data read")
  198 + else:
  199 + channel_data = []
  200 + for i in range(0, num_readings):
  201 + temperature = temperature_buffer[i]
  202 + # remember to conver time from relative to absolute timestamp
  203 + time_ms = self.convert_time(times_ms_buffer[i])
  204 + channel_data.append((temperature, time_ms, overflow.value))
  205 + data.append(channel_data)
  206 + return data
temperature_logger.py View file @ 02a86c9
  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 +
  93 + def start_monitor(self):
  94 + self.setup()
  95 + self.start()
  96 +
  97 + def cancelISR(self, *args, **kwargs):
  98 + dprint("cancelISR called")
  99 + self.cancel()
  100 + for device in self.tc08s:
  101 + device.stop()
  102 + device.close()
  103 +
  104 +
  105 +if __name__ == "__main__":
  106 + monitor = Monitor(LOG_INTERVAL_SEC)
  107 + monitor.start_monitor()
  108 + dprint("monitor running")
  109 + signal.signal(signal.SIGINT, monitor.cancelISR)
  110 + signal.signal(signal.SIGTERM, monitor.cancelISR)
  111 + monitor.join()
temperature_logger.service View file @ 02a86c9
  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 +
  16 +[Install]
  17 +WantedBy=multi-user.target