From 610e1d169b24921c8c5ec11c73a03ce63cfb8609 Mon Sep 17 00:00:00 2001 From: jbernardis Date: Wed, 6 May 2015 19:37:51 -0400 Subject: [PATCH] Configtool: add Steinhart-Hart algorithm and thermistor presets. The Steinhart-Hart algorithm allows more precise thermistor tables, but also requires more parameters, which aren't available for all thermistors. Accordingly, add support for both, the traditional logic using the beta parameter as well as the new one. This also adds thermistor-presets, so users can simply choose from a pulldown-menu to set their thermistor. Also, identical thermistors get merged into one table, saving binary size. Last not least, a few bugs in this area got fixed. Usually, all these things go into separate commits, but they were contributed all in one and separating them is a bit error-prone for little gain. This should address issue #130, #134 and #135. --- configtool.default.ini | 20 ++ configtool/addsensordlg.py | 525 ++++++++++++++++++++++++------ configtool/boardpanel.py | 28 +- configtool/data.py | 3 +- configtool/sensorlist.py | 5 +- configtool/sensorpage.py | 11 +- configtool/settings.py | 21 ++ configtool/thermistor.py | 88 +++++ configtool/thermistortablefile.py | 158 +++++---- 9 files changed, 679 insertions(+), 180 deletions(-) create mode 100644 configtool/thermistor.py diff --git a/configtool.default.ini b/configtool.default.ini index 5c887ab..97d1bf5 100644 --- a/configtool.default.ini +++ b/configtool.default.ini @@ -46,3 +46,23 @@ r1 = 0 numtemps = 25 maxadc = 1023 minadc = 0 + + +# Define thermistor presets. These can either be defined to use the +# beta argorithm by specifying 4 parameters: +# R0, beta, Rp, Vadc +# +# or to use the Steinhart-Hart algorithm by specifying 7 parameters: +# Rp, T0, R0, T1, R1, T2, R2 +# +# TODO: this should be moved into a file which isn't overridden by a local +# file (here: configtool.ini). Similar to config/protectedfiles.py. +[thermistors] +EPCOS 100K (B5754061104) = 100000, 4066, 4700, 5.0 +EPCOS 100K (B57560G1104F) = 100000, 4092, 4700, 5.0 +EPCOS 100K (B57560G104F) = 100000, 4036, 4700, 5.0 +RRRF 100K = 100000, 3960, 4700, 5.0 +RRRF 10K = 10000, 3964, 1600, 5.0 +RS 10K = 10000, 3480, 1600, 5.0 +Honeywell 100K = 100000, 3974, 4700, 5.0 +ATC Semitec 104GT-2 = 100000, 4267, 4700, 5.0 diff --git a/configtool/addsensordlg.py b/configtool/addsensordlg.py index 1124c6a..cdd5df3 100644 --- a/configtool/addsensordlg.py +++ b/configtool/addsensordlg.py @@ -3,11 +3,19 @@ import wx from configtool.data import (pinNames, BSIZESMALL, sensorTypes, offsetTcLabel, offsetChLabel, reInteger, reFloat) +MODE_NONTHERM = 0 +MODE_THERMISTOR = 1 + +METHOD_BETA = 0 +METHOD_SH = 1 +MethodLabels = ["Beta", "Steinhart-Hart"] + +labelWidth = 160 + class AddSensorDlg(wx.Dialog): def __init__(self, parent, names, pins, font, name = "", stype = "", - pin = "", r0 = "", beta = "", r2 = "", vadc = "", - modify = False): + pin = "", params = [], modify = False): if modify: title = "Modify temperature sensor" else: @@ -19,14 +27,28 @@ class AddSensorDlg(wx.Dialog): self.names = names self.choices = pins self.modify = modify + self.presets = parent.thermistorpresets - labelWidth = 160 + if len(params) == 0: + self.currentMethod = METHOD_BETA + self.currentMode = MODE_NONTHERM + else: + self.currentMode = MODE_THERMISTOR + if len(params) == 4: + self.currentMethod = METHOD_BETA + else: + self.currentMethod = METHOD_SH self.nameValid = False - self.R0Valid = False - self.betaValid = False - self.R2Valid = False - self.vadcValid = False + self.param0Valid = False + self.param1Valid = False + self.param2Valid = False + self.param3Valid = False + self.param4Valid = False + self.param5Valid = False + self.param6Valid = False + + sizer = wx.BoxSizer(wx.VERTICAL) hsz = wx.BoxSizer(wx.HORIZONTAL) hsz.AddSpacer((10, 10)) @@ -75,6 +97,7 @@ class AddSensorDlg(wx.Dialog): if not found: ch.SetSelection(0) stStart = sl[0] + self.chType = ch lsz.Add(ch) @@ -103,76 +126,161 @@ class AddSensorDlg(wx.Dialog): csz.AddSpacer((10, 10)) lsz = wx.BoxSizer(wx.HORIZONTAL) - st = wx.StaticText(self, wx.ID_ANY, "R0:", size = (labelWidth, -1), + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), style = wx.ALIGN_RIGHT) st.SetFont(font) lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label0 = st - self.tcR0 = wx.TextCtrl(self, wx.ID_ANY, r0) - self.tcR0.SetFont(font) - self.tcR0.Bind(wx.EVT_TEXT, self.onR0Entry) - lsz.Add(self.tcR0) - self.tcR0.SetToolTipString("Nominal resistance of the thermistor. " - "Typically 10000 ( = 10k) or 100000 ( = 100k).") - - csz.Add(lsz) - csz.AddSpacer((10, 10)) - - lsz = wx.BoxSizer(wx.HORIZONTAL) - st = wx.StaticText(self, wx.ID_ANY, "Beta:", size = (labelWidth, -1), - style = wx.ALIGN_RIGHT) - st.SetFont(font) - lsz.Add(st, 1, wx.TOP, offsetTcLabel) - - self.tcBeta = wx.TextCtrl(self, wx.ID_ANY, beta) - self.tcBeta.SetFont(font) - self.tcBeta.Bind(wx.EVT_TEXT, self.onBetaEntry) - lsz.Add(self.tcBeta) - self.tcBeta.SetToolTipString("Thermistor beta value. Can be found in the " - "datasheet or measured like described in http" - "://reprap.org/wiki/MeasuringThermistorBeta") + vals = params + ["", "", "", "", "", "", ""] + self.param0 = wx.TextCtrl(self, wx.ID_ANY, vals[0]) + self.param0.SetFont(font) + self.param0.Bind(wx.EVT_TEXT, self.onParam0Entry) + lsz.Add(self.param0) csz.Add(lsz) csz.AddSpacer((10, 10)) lsz = wx.BoxSizer(wx.HORIZONTAL) - st = wx.StaticText(self, wx.ID_ANY, "R2:", size = (labelWidth, -1), + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), style = wx.ALIGN_RIGHT) st.SetFont(font) lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label1 = st - self.tcR2 = wx.TextCtrl(self, wx.ID_ANY, r2) - self.tcR2.SetFont(font) - self.tcR2.Bind(wx.EVT_TEXT, self.onR2Entry) - lsz.Add(self.tcR2) - self.tcR2.SetToolTipString("Resistance value of the secondary resistor. " - "This is not a property of the thermistor, but " - "one of the board. Typical values are 4700 " - "( = 4k7 ohms) or 1000 ( = 1k ohms).\n\n" - "As these resistors are typically +-5%, " - "measuring the actually used one can increase " - "accuracy substantially.") + self.param1 = wx.TextCtrl(self, wx.ID_ANY, vals[1]) + self.param1.SetFont(font) + self.param1.Bind(wx.EVT_TEXT, self.onParam1Entry) + lsz.Add(self.param1) csz.Add(lsz) csz.AddSpacer((10, 10)) lsz = wx.BoxSizer(wx.HORIZONTAL) - st = wx.StaticText(self, wx.ID_ANY, "Vadc:", size = (labelWidth, -1), + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), style = wx.ALIGN_RIGHT) st.SetFont(font) lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label2 = st - self.tcVadc = wx.TextCtrl(self, wx.ID_ANY, vadc) - self.tcVadc.SetFont(font) - self.tcVadc.Bind(wx.EVT_TEXT, self.onVadcEntry) - lsz.Add(self.tcVadc) - self.tcVadc.SetToolTipString("Comparison voltage used by the controller. " - "Usually the same as the controller's supply " - "voltage, 3.3 or 5.0 (volts).") + self.param2 = wx.TextCtrl(self, wx.ID_ANY, vals[2]) + self.param2.SetFont(font) + self.param2.Bind(wx.EVT_TEXT, self.onParam2Entry) + lsz.Add(self.param2) csz.Add(lsz) csz.AddSpacer((10, 10)) + lsz = wx.BoxSizer(wx.HORIZONTAL) + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), + style = wx.ALIGN_RIGHT) + st.SetFont(font) + lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label3 = st + + self.param3 = wx.TextCtrl(self, wx.ID_ANY, vals[3]) + self.param3.SetFont(font) + self.param3.Bind(wx.EVT_TEXT, self.onParam3Entry) + lsz.Add(self.param3) + + csz.Add(lsz) + csz.AddSpacer((10, 10)) + + lsz = wx.BoxSizer(wx.HORIZONTAL) + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), + style = wx.ALIGN_RIGHT) + st.SetFont(font) + lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label4 = st + + self.param4 = wx.TextCtrl(self, wx.ID_ANY, vals[4]) + self.param4.SetFont(font) + self.param4.Bind(wx.EVT_TEXT, self.onParam4Entry) + lsz.Add(self.param4) + + csz.Add(lsz) + csz.AddSpacer((10, 10)) + + lsz = wx.BoxSizer(wx.HORIZONTAL) + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), + style = wx.ALIGN_RIGHT) + st.SetFont(font) + lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label5 = st + + self.param5 = wx.TextCtrl(self, wx.ID_ANY, vals[5]) + self.param5.SetFont(font) + self.param5.Bind(wx.EVT_TEXT, self.onParam5Entry) + lsz.Add(self.param5) + + csz.Add(lsz) + csz.AddSpacer((10, 10)) + + lsz = wx.BoxSizer(wx.HORIZONTAL) + st = wx.StaticText(self, wx.ID_ANY, "", size = (labelWidth, -1), + style = wx.ALIGN_RIGHT) + st.SetFont(font) + lsz.Add(st, 1, wx.TOP, offsetTcLabel) + self.label6 = st + + self.param6 = wx.TextCtrl(self, wx.ID_ANY, vals[6]) + self.param6.SetFont(font) + self.param6.Bind(wx.EVT_TEXT, self.onParam6Entry) + lsz.Add(self.param6) + + csz.Add(lsz) + csz.AddSpacer((10, 10)) + + csz.AddSpacer((10, 10)) + + hsz.Add(csz) + hsz.AddSpacer((10, 10)) + + csz = wx.BoxSizer(wx.VERTICAL) + csz.AddSpacer((30, 45)) + + lsz = wx.BoxSizer(wx.HORIZONTAL) + st = wx.StaticText(self, wx.ID_ANY, "Presets:", + size = (70, -1), style = wx.ALIGN_RIGHT) + st.SetFont(font) + lsz.Add(st, 1, wx.TOP, offsetTcLabel) + + choices = [""] + sorted(self.presets.keys()) + ch = wx.Choice(self, wx.ID_ANY, choices = choices) + ch.SetFont(font) + ch.Enable(False) + ch.SetSelection(0) + self.chPresets = ch + ch.Bind(wx.EVT_CHOICE, self.onPresetChoice) + lsz.Add(ch) + + csz.Add(lsz) + csz.AddSpacer((10, 50)) + + b = wx.StaticBox(self, wx.ID_ANY, "Temp Table Algorithm") + b.SetFont(font) + sbox = wx.StaticBoxSizer(b, wx.VERTICAL) + sbox.AddSpacer((5, 5)) + style = wx.RB_GROUP + self.rbMethod = [] + for k in MethodLabels: + rb = wx.RadioButton(self, wx.ID_ANY, k, style = style) + rb.SetFont(font) + self.Bind(wx.EVT_RADIOBUTTON, self.onMethodSelect, rb) + self.rbMethod.append(rb) + style = 0 + + sbox.Add(rb, 1, wx.LEFT + wx.RIGHT, 16) + sbox.AddSpacer((5, 5)) + + self.rbMethod[self.currentMethod].SetValue(True); + csz.Add(sbox) + + hsz.Add(csz) + hsz.AddSpacer((10, 10)) + + sizer.Add(hsz) + bsz = wx.BoxSizer(wx.HORIZONTAL) self.bSave = wx.Button(self, wx.ID_ANY, "Save", size = BSIZESMALL) @@ -188,13 +296,10 @@ class AddSensorDlg(wx.Dialog): self.bCancel.Bind(wx.EVT_BUTTON, self.onCancel) bsz.Add(self.bCancel) - csz.Add(bsz, flag = wx.ALIGN_CENTER_HORIZONTAL) - csz.AddSpacer((10, 10)) + sizer.Add(bsz, flag = wx.ALIGN_CENTER_HORIZONTAL) + sizer.AddSpacer((10, 10)) - hsz.Add(csz) - hsz.AddSpacer((10, 10)) - - self.SetSizer(hsz) + self.SetSizer(sizer) self.Fit() self.selectSensorType(stStart) @@ -222,9 +327,20 @@ class AddSensorDlg(wx.Dialog): tc.SetBackgroundColour("pink") tc.Refresh() + def onMethodSelect(self, evt): + rb = evt.GetEventObject() + lbl = rb.GetLabel() + for i in range(len(MethodLabels)): + if lbl == MethodLabels[i]: + self.currentMethod = i + self.setDialogMode() + self.validateFields() + return + def checkDlgValidity(self): - if (self.nameValid and self.R0Valid and self.betaValid and - self.R2Valid and self.vadcValid): + if (self.nameValid and self.param0Valid and self.param1Valid and + self.param2Valid and self.param3Valid and self.param4Valid and + self.param5Valid and self.param6Valid): self.bSave.Enable(True) else: self.bSave.Enable(False) @@ -242,7 +358,7 @@ class AddSensorDlg(wx.Dialog): valid = True else: valid = False - + if valid: tc.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) else: @@ -250,7 +366,7 @@ class AddSensorDlg(wx.Dialog): tc.Refresh() return valid - + def onTextCtrlFloat(self, tc, rqd): w = tc.GetValue().strip() if w == "": @@ -264,7 +380,7 @@ class AddSensorDlg(wx.Dialog): valid = True else: valid = False - + if valid: tc.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) else: @@ -272,64 +388,256 @@ class AddSensorDlg(wx.Dialog): tc.Refresh() return valid - def onR0Entry(self, evt): - stype = self.chType.GetString(self.chType.GetSelection()) - if stype in ['Thermistor']: - rqd = True + def onParam0Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + self.param0Valid = self.onTextCtrlInteger(self.param0, True) else: - rqd = False - self.R0Valid = self.onTextCtrlInteger(self.tcR0, rqd) + self.param0Valid = True + self.checkDlgValidity() if evt is not None: evt.Skip() - def onBetaEntry(self, evt): - stype = self.chType.GetString(self.chType.GetSelection()) - if stype in ['Thermistor']: - rqd = True + def onParam1Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + self.param1Valid = self.onTextCtrlInteger(self.param1, True) else: - rqd = False - self.betaValid = self.onTextCtrlInteger(self.tcBeta, rqd) + self.param1Valid = True + self.checkDlgValidity() if evt is not None: evt.Skip() - def onR2Entry(self, evt): - stype = self.chType.GetString(self.chType.GetSelection()) - if stype in ['Thermistor']: - rqd = True + def onParam2Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + self.param2Valid = self.onTextCtrlInteger(self.param2, True) else: - rqd = False - self.R2Valid = self.onTextCtrlInteger(self.tcR2, rqd) + self.param2Valid = True + self.checkDlgValidity() if evt is not None: evt.Skip() - def onVadcEntry(self, evt): - stype = self.chType.GetString(self.chType.GetSelection()) - if stype in ['Thermistor']: - rqd = True + def onParam3Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + self.param3Valid = self.onTextCtrlFloat(self.param3, True) + else: + self.param3Valid = self.onTextCtrlInteger(self.param3, True) else: - rqd = False - self.vadcValid = self.onTextCtrlFloat(self.tcVadc, rqd) + self.param3Valid = True + + self.checkDlgValidity() + if evt is not None: + evt.Skip() + + def onParam4Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + self.param4Valid = True + else: + self.param4Valid = self.onTextCtrlInteger(self.param4, True) + else: + self.param4Valid = True + + self.checkDlgValidity() + if evt is not None: + evt.Skip() + + def onParam5Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + self.param5Valid = True + else: + self.param5Valid = self.onTextCtrlInteger(self.param5, True) + else: + self.param5Valid = True + + self.checkDlgValidity() + if evt is not None: + evt.Skip() + + def onParam6Entry(self, evt): + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + self.param6Valid = True + else: + self.param6Valid = self.onTextCtrlInteger(self.param6, True) + else: + self.param6Valid = True + self.checkDlgValidity() if evt is not None: evt.Skip() def selectSensorType(self, lbl): - if lbl == 'Thermistor': - flag = True + if lbl == "Thermistor": + self.currentMode = MODE_THERMISTOR else: - flag = False + self.currentMode = MODE_NONTHERM + self.setDialogMode() - self.tcR0.Enable(flag); - self.tcBeta.Enable(flag); - self.tcR2.Enable(flag); - self.tcVadc.Enable(flag); + def setDialogMode(self): + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + self.param0.SetToolTipString("Nominal resistance of the thermistor. " + "Typically 10000 ( = 10k) or 100000 " + "( = 100k).") + self.label0.SetLabel("R0:") + self.param1.SetToolTipString("Thermistor beta value. Can be found in " + "the datasheet or measured like described " + "in http://reprap.org/wiki/" + "MeasuringThermistorBeta") + self.label1.SetLabel("Beta:") + self.param2.SetToolTipString("Resistance value of the secondary " + "resistor. This is not a property of the " + "thermistor, but one of the board. " + "Typical values are 4700 ( = 4k7 ohms) " + "or 1000 ( = 1k ohms).") + self.label2.SetLabel("R2:") + self.param3.SetToolTipString("Comparison voltage used by the " + "controller. Usually the same as the " + "controller's supply voltage, 3.3 or 5.0 " + "(volts).") + self.label3.SetLabel("Vadc:") + self.label4.SetLabel("") + self.param4.SetToolTip(None) + self.param4.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param4.Refresh() + self.label5.SetLabel("") + self.param5.SetToolTip(None) + self.param5.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param5.Refresh() + self.label6.SetLabel("") + self.param6.SetToolTip(None) + self.param6.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param6.Refresh() + self.param4.Enable(False) + self.param5.Enable(False) + self.param6.Enable(False) + else: + self.param0.SetToolTipString("Reference resistance value.") + self.label0.SetLabel("Rp:") + self.param1.SetToolTipString("First data point, temperature at which " + "resistance is equal to R0.") + self.label1.SetLabel("T0:") + self.param2.SetToolTipString("Resistance when temperature is T0.") + self.label2.SetLabel("R0:") + self.param3.SetToolTipString("Second data point, temperature at which " + "resistance is equal to R1.") + self.label3.SetLabel("T1:") + self.param4.SetToolTipString("Resistance when temperature is T1.") + self.label4.SetLabel("R1:") + self.param5.SetToolTipString("Third data point, temperature at which " + "resistance is equal to R2.") + self.label5.SetLabel("T2:") + self.param6.SetToolTipString("Resistance when temperature is T2.") + self.label6.SetLabel("R2:") + self.param4.Enable(True) + self.param5.Enable(True) + self.param6.Enable(True) + + self.label0.SetSize((labelWidth, -1)) + self.label0.SetWindowStyle(wx.ALIGN_RIGHT) + self.label1.SetSize((labelWidth, -1)) + self.label1.SetWindowStyle(wx.ALIGN_RIGHT) + self.label2.SetSize((labelWidth, -1)) + self.label2.SetWindowStyle(wx.ALIGN_RIGHT) + self.label3.SetSize((labelWidth, -1)) + self.label3.SetWindowStyle(wx.ALIGN_RIGHT) + self.label4.SetSize((labelWidth, -1)) + self.label4.SetWindowStyle(wx.ALIGN_RIGHT) + self.label5.SetSize((labelWidth, -1)) + self.label5.SetWindowStyle(wx.ALIGN_RIGHT) + self.label6.SetSize((labelWidth, -1)) + self.label6.SetWindowStyle(wx.ALIGN_RIGHT) + + self.param0.Enable(True); + self.param1.Enable(True); + self.param2.Enable(True); + self.param3.Enable(True); + self.chPresets.Enable(True); + for rb in self.rbMethod: + rb.Enable(True) + else: + self.param0.SetToolTip(None) + self.label0.SetLabel("") + self.param0.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param0.Refresh() + + self.param1.SetToolTip(None) + self.label1.SetLabel("") + self.param1.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param1.Refresh() + + self.param2.SetToolTip(None) + self.label2.SetLabel("") + self.param2.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param2.Refresh() + + self.param3.SetToolTip(None) + self.label3.SetLabel("") + self.param3.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param3.Refresh() + + self.param4.SetToolTip(None) + self.label4.SetLabel("") + self.param4.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param4.Refresh() + + self.param5.SetToolTip(None) + self.label5.SetLabel("") + self.param5.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param5.Refresh() + + self.param6.SetToolTip(None) + self.label6.SetLabel("") + self.param6.SetBackgroundColour( + wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + self.param6.Refresh() + + self.param0.Enable(False); + self.param1.Enable(False); + self.param2.Enable(False); + self.param3.Enable(False); + self.param4.Enable(False); + self.param5.Enable(False); + self.param6.Enable(False); + self.chPresets.Enable(False); + for rb in self.rbMethod: + rb.Enable(False) def onChoice(self, evt): pass + def onPresetChoice(self, evt): + ch = evt.GetEventObject() + s = ch.GetSelection() + label = ch.GetString(s) + if s != 0: + self.param0.SetValue(self.presets[label][0]) + self.param1.SetValue(self.presets[label][1]) + self.param2.SetValue(self.presets[label][2]) + self.param3.SetValue(self.presets[label][3]) + if len(self.presets[label]) == 7: + self.param4.SetValue(self.presets[label][4]) + self.param5.SetValue(self.presets[label][5]) + self.param6.SetValue(self.presets[label][5]) + self.currentMethod = METHOD_SH + else: + self.currentMethod = METHOD_BETA + self.rbMethod[self.currentMethod].SetValue(True) + self.setDialogMode() + + self.validateFields() + evt.Skip() + def onSensorType(self, evt): ch = evt.GetEventObject() s = ch.GetSelection() @@ -341,21 +649,34 @@ class AddSensorDlg(wx.Dialog): def validateFields(self): self.validateName(self.tcName) - self.onR0Entry(None) - self.onBetaEntry(None) - self.onR2Entry(None) - self.onVadcEntry(None) + self.onParam0Entry(None) + self.onParam1Entry(None) + self.onParam2Entry(None) + self.onParam3Entry(None) + self.onParam4Entry(None) + self.onParam5Entry(None) + self.onParam6Entry(None) def getValues(self): nm = self.tcName.GetValue() pin = self.choices[self.chPin.GetSelection()] stype = self.chType.GetString(self.chType.GetSelection()) - - if stype in ['Thermistor']: - addtl = [self.tcR0.GetValue().strip(), self.tcBeta.GetValue().strip(), - self.tcR2.GetValue().strip(), self.tcVadc.GetValue().strip()] + if self.currentMode == MODE_THERMISTOR: + if self.currentMethod == METHOD_BETA: + addtl = [str(self.param0.GetValue().strip()), + str(self.param1.GetValue().strip()), + str(self.param2.GetValue().strip()), + str(self.param3.GetValue().strip())] + else: + addtl = [str(self.param0.GetValue().strip()), + str(self.param1.GetValue().strip()), + str(self.param2.GetValue().strip()), + str(self.param3.GetValue().strip()), + str(self.param4.GetValue().strip()), + str(self.param5.GetValue().strip()), + str(self.param6.GetValue().strip())] else: - addtl = "NONE" + addtl = None return (nm, sensorTypes[stype], pin, addtl) diff --git a/configtool/boardpanel.py b/configtool/boardpanel.py index e97a57d..f78c643 100644 --- a/configtool/boardpanel.py +++ b/configtool/boardpanel.py @@ -11,7 +11,8 @@ from configtool.data import (defineValueFormat, reCandProcessors, reCandCPUClocks, reFloatAttr, reDefine, reDefineBL, reDefQS, reDefQSm, reDefQSm2, reDefBool, reDefBoolBL, reDefHT, - reDefTS, reDefTT, reSensor, reHeater, reTempTable) + reDefTS, reDefTT, reSensor, reHeater, + reTempTable4, reTempTable7) from configtool.pinoutspage import PinoutsPage from configtool.sensorpage import SensorsPage from configtool.heaterspage import HeatersPage @@ -66,7 +67,8 @@ class BoardPanel(wx.Panel): self.pageValid.append(True) self.pgSensors = SensorsPage(self, self.nb, len(self.pages), - self.settings.font) + self.settings.font, + self.settings.thermistorpresets) text = "Temperature Sensors" self.nb.AddPage(self.pgSensors, text) self.pages.append(self.pgSensors) @@ -356,6 +358,8 @@ class BoardPanel(wx.Panel): tn = self.sensors[k][0].upper() if tn in tempTables.keys(): self.sensors[k][3] = tempTables[tn] + else: + self.sensors[k][3] = None if os.path.basename(fn) in protectedFiles: self.parent.enableSaveBoard(False, True) @@ -395,11 +399,16 @@ class BoardPanel(wx.Panel): return None def parseTempTable(self, s): - m = reTempTable.search(s) + m = reTempTable4.search(s) if m: t = m.groups() if len(t) == 4: return list(t) + m = reTempTable7.search(s) + if m: + t = m.groups() + if len(t) == 7: + return list(t) return None def onSaveConfig(self, evt): @@ -482,14 +491,13 @@ class BoardPanel(wx.Panel): ttString = "\n// r0 beta r2 vadc\n" for s in self.sensors: sstr = "%-10s%-15s%-7s" % ((s[0] + ","), (s[1] + ","), (s[2] + ",")) - if s[3] != "NONE": + if s[3] is None: + sstr += "0" + else: tt = s[3] sstr += "THERMISTOR_%s" % s[0].upper() - ttString += "//TEMP_TABLE %-8s (%-8s%-6s%-6s%s)\n" % \ - (s[0].upper(), (tt[0] + ","), (tt[1] + ","), - (tt[2] + ","), tt[3]) - else: - sstr += s[3] + ttString += "//TEMP_TABLE %-8s (%s)\n" % \ + (s[0].upper(), ", ".join(tt)) fp.write("DEFINE_TEMP_SENSOR(%s)\n" % sstr) fp.write(ttString) skipToSensorEnd = True @@ -607,7 +615,9 @@ class BoardPanel(wx.Panel): dlg.Destroy() fp.close() + return self.generateTempTables() + def generateTempTables(self): if not generateTempTables(self.sensors, self.settings): dlg = wx.MessageDialog(self, "Error writing to file thermistortable.h.", "File error", wx.OK + wx.ICON_ERROR) diff --git a/configtool/data.py b/configtool/data.py index c0b7bec..fd03846 100644 --- a/configtool/data.py +++ b/configtool/data.py @@ -57,7 +57,8 @@ reHelpTextEnd = re.compile("^\s*\*/") reSensor = re.compile(".*\\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\\)") reHeater = re.compile(".*\\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\\)") -reTempTable = re.compile(".*\\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d*.?\d*)\s*\\)") +reTempTable4 = re.compile(".*\\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d*.?\d*)\s*\\)") +reTempTable7 = re.compile(".*\\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\\)") reInteger = re.compile("^\d+U?L?$") reFloat = re.compile("^\d+(\.\d*)?$") diff --git a/configtool/sensorlist.py b/configtool/sensorlist.py index 9f1f72c..7363cc3 100644 --- a/configtool/sensorlist.py +++ b/configtool/sensorlist.py @@ -85,7 +85,10 @@ class SensorList(wx.ListCtrl): elif len(s) == 3: return "" else: - return s[3] + if s[3] is None: + return "" + else: + return "[%s]" % (", ".join(s[3])) def OnGetItemAttr(self, item): if not self.valid[item]: diff --git a/configtool/sensorpage.py b/configtool/sensorpage.py index dfe6da8..900fe93 100644 --- a/configtool/sensorpage.py +++ b/configtool/sensorpage.py @@ -7,11 +7,12 @@ from addsensordlg import AddSensorDlg class SensorsPage(wx.Panel, Page): - def __init__(self, parent, nb, idPg, font): + def __init__(self, parent, nb, idPg, font, thermistorpresets): wx.Panel.__init__(self, nb, wx.ID_ANY) Page.__init__(self, font) self.parent = parent self.font = font + self.thermistorpresets = thermistorpresets self.id = idPg self.sensorTypeKeys = {'TT_MAX6675': 'TEMP_MAX6675', @@ -101,10 +102,14 @@ class SensorsPage(wx.Panel, Page): nm.append(s[0]) s = self.sensors[self.selection] + if s[3] is None: + params = [] + else: + params = s[3] + dlg = AddSensorDlg(self, nm, self.validPins, self.font, name = s[0], stype = s[1], pin = s[2], - r0 = s[3][0], beta = s[3][1], r2 = s[3][2], - vadc = s[3][3], modify = True) + params = params, modify = True) rc = dlg.ShowModal() if rc == wx.ID_OK: tt = dlg.getValues() diff --git a/configtool/settings.py b/configtool/settings.py index 09ab8e9..0864e79 100644 --- a/configtool/settings.py +++ b/configtool/settings.py @@ -29,6 +29,8 @@ class Settings: self.maxAdc = 1023 self.minAdc = 1 + self.thermistorpresets = {} + self.cfg = ConfigParser.ConfigParser() self.cfg.optionxform = str @@ -70,6 +72,16 @@ class Settings: else: print "Missing %s section - assuming defaults." % self.section + section = "thermistors" + if self.cfg.has_section(section): + for opt, value in self.cfg.items(section): + value = value.strip().replace(" ", "") + vl = value.split(",") + if len(vl) != 4 and len(vl) != 7: + print "Invalid entry for thermistor %s." % opt + else: + self.thermistorpresets[opt] = vl + def saveSettings(self): self.section = "configtool" try: @@ -90,6 +102,15 @@ class Settings: self.cfg.set(self.section, "minadc", str(self.minAdc)) self.cfg.set(self.section, "uploadspeed", str(self.uploadspeed)) + section = "thermistors" + try: + self.cfg.add_section(section) + except ConfigParser.DuplicateSectionError: + pass + + for t in self.thermistorpresets.keys(): + self.cfg.set(section, t, ", ".join(self.thermistorpresets[t])) + try: cfp = open(self.inifile, 'wb') except: diff --git a/configtool/thermistor.py b/configtool/thermistor.py new file mode 100644 index 0000000..91e3430 --- /dev/null +++ b/configtool/thermistor.py @@ -0,0 +1,88 @@ + +from math import * +import sys + + +class SHThermistor: + def __init__(self, rp, t0, r0, t1, r1, t2, r2): + self.rp = rp + + self.paramsOK = True + try: + T0 = t0 + 273.15; T1 = t1 + 273.15; T2 = t2 + 273.15 + a0 = log(r0); a1 = log(r1); a2 = log(r2) + z = a0 - a1 + y = a0 - a2 + x = 1 / T0 - 1 / T1 + w = 1 / T0 - 1 / T2 + v = a0 ** 3 - a1 ** 3 + u = a0 ** 3 - a2 ** 3 + + self.C = (x - z * w / y) / (v - z * u / y) + self.B = (x - self.C * v) / z + self.A = 1 / T0 - self.C * a0 ** 3 - self.B * a0 + except: + self.paramsOK = False + + def setting(self, t): + if not self.paramsOK: + return None, None + + try: + T = t + 273.15 + y = (self.A - 1/T) / self.C + x = ((self.B / (3 * self.C)) ** 3 + (y ** 2) / 4) ** 0.5 + r = exp((x - y / 2) ** (1.0/3) - (x + y / 2) ** (1.0/3)) + return self.adc(r), r + except: + return None, None + + def adc(self, r): + return 1023.0 * r / (r + self.rp) + + +class BetaThermistor: + def __init__(self, r0, t0, beta, r1, r2, vadc): + self.paramsOK = True + + try: + self.r0 = r0 + self.t0 = t0 + 273.15 + self.beta = beta + self.vadc = vadc + self.k = r0 * exp(-beta / self.t0) + + if r1 > 0: + self.vs = r1 * self.vadc / (r1 + r2) + self.rs = r1 * r2 / (r1 + r2) + else: + self.vs = self.vadc + self.rs = r2 + except: + self.paramsOK = False + + def temp(self, adc): + v = adc * self.vadc / 1024 + if (self.vs - v): + r = self.rs * v / (self.vs - v) + else: + r = self.r0 * 10 + try: + return (self.beta / log(r / self.k)) - 273.15 + except: + print "// error for ADC = {adc}, {v}, {r}".format(adc = adc, v = v, r = r) + return None + + def resistance(self, t): + return self.r0 * exp(self.beta * (1 / (t + 273.15) - 1 / self.t0)) + + def setting(self, t): + if not self.paramsOK: + return None, None + + try: + r = self.r0 * exp(self.beta * (1 / (t + 273.15) - 1 / self.t0)) + v = self.vs * r / (self.rs + r) + return round(v / self.vadc * 1024), r + except: + return None, None diff --git a/configtool/thermistortablefile.py b/configtool/thermistortablefile.py index 78f3225..81b6afc 100644 --- a/configtool/thermistortablefile.py +++ b/configtool/thermistortablefile.py @@ -1,6 +1,6 @@ import os -from createTemperatureLookup import Thermistor +from thermistor import SHThermistor, BetaThermistor class ThermistorTableFile: @@ -18,21 +18,30 @@ class ThermistorTableFile: def output(self, text): self.fp.write(text + "\n") +def paramsEqual(p1, p2): + for i in range(len(p1)): + if p1[i] != p2[i]: + return False + + return True def generateTempTables(sensors, settings): ofp = ThermistorTableFile(settings.folder) if ofp.error: return False - mult = 4 - minadc = int(settings.minAdc) N = int(settings.numTemps) - # Make a list of single-item dicts to keep the order. tl = [] for sensor in sensors: - if sensor[3] != "NONE": - tl.append({sensor[0].upper(): sensor[3]}) + if sensor[3] is not None: + found = False + for t in tl: + if paramsEqual(t[0], sensor[3]): + t[1].append(sensor[0].upper()) + found = True + if not found: + tl.append((sensor[3], [sensor[0].upper()])) ofp.output(""); ofp.output("/**"); @@ -48,77 +57,98 @@ def generateTempTables(sensors, settings): ofp.output(""); for i in range(len(tl)): - ofp.output("#define THERMISTOR_%s %d" % (tl[i].keys()[0], i)) + for n in tl[i][1]: + ofp.output("#define THERMISTOR_%s %d" % (n, i)) ofp.output(""); if len(tl) == 0 or N == 0: ofp.close(); return True + step = int((300.0 / (N-1) + 1)) + idx = range(step*(N-1), -step, -step) + ofp.output("const uint16_t PROGMEM temptable[NUMTABLES][NUMTEMPS][2] = {") tcount = 0 for tn in tl: tcount += 1 - t = tn.values()[0] - r0 = t[0] - beta = t[1] - r2 = t[2] - vadc = t[3] - ofp.output(" // %s temp table parameters:" % tn.keys()[0]) - ofp.output((" // R0 = %s, T0 = %s, R1 = %s, R2 = %s, beta = %s, " - "maxadc = %s") % (r0, settings.t0, settings.r1, r2, - beta, settings.maxAdc)) - ofp.output(" {") - thm = Thermistor(int(r0), - int(settings.t0), - int(beta), - int(settings.r1), - int(r2), - float(vadc), - float(vadc)) - maxadc = int(settings.maxAdc) - zadc = int(thm.setting(0)) - if zadc < maxadc: - maxadc = zadc - - increment = float(maxadc - minadc) / float(N - 1); - ct = 1.0 - adcs = [] - for i in range(N): - adcs.append(int(ct)) - ct += increment - if ct > maxadc: - ct = maxadc - - counter = 0 - for adc in adcs: - counter = counter + 1 - degC = thm.temp(adc) - resistance = thm.resistance(thm.temp(adc)) - vTherm = adc * thm.vadc / 1024 - ptherm = vTherm * vTherm / resistance - if adc > 1: - resolution = thm.temp(adc - 1) - thm.temp(adc) - else: - resolution = thm.temp(adc) - thm.temp(adc + 1) - if counter == len(adcs): - sep = " " - else: - sep = "," - val = int(thm.temp(adc) * mult) - if val < 0: - val = 0 - ostr = (" {%4s, %5s}%s // %6.2f C, %6.0f ohms, %0.3f V," - " %0.2f C/count, %0.2f mW") % (adc, val, - sep, degC, resistance, vTherm, resolution, ptherm * 1000) - ofp.output(ostr) - if tcount == len(tl): - ofp.output(" }") + finalTable = tcount == len(tl) + if len(tn[0]) == 4: + BetaTable(ofp, tn[0], tn[1], idx, settings, finalTable) + elif len(tn[0]) == 7: + SteinhartHartTable(ofp, tn[0], tn[1], idx, settings, finalTable) else: - ofp.output(" },") + pass ofp.output("};") ofp.close() - return True + +def BetaTable(ofp, params, names, idx, settings, finalTable): + r0 = params[0] + beta = params[1] + r2 = params[2] + vadc = float(params[3]) + ofp.output(" // %s temp table using Beta algorithm with parameters:" % + (", ".join(names))) + ofp.output((" // R0 = %s, T0 = %s, R1 = %s, R2 = %s, beta = %s, " + "maxadc = %s") % (r0, settings.t0, settings.r1, r2, + beta, settings.maxAdc)) + ofp.output(" {") + + thrm = BetaThermistor(int(r0), int(settings.t0), int(beta), int(settings.r1), + int(r2), vadc) + + for t in idx: + a, r = thrm.setting(t) + if a is None: + ofp.output("// ERROR CALCULATING THERMISTOR VALUES AT TEMPERATURE %d" % t) + continue + + vTherm = a * vadc / 1024 + ptherm = vTherm * vTherm / r + if t <= 0: + c = " " + else: + c = "," + ostr = (" {%4s, %5s}%s // %4d C, %6.0f ohms, %0.3f V," + " %0.2f mW") % (int(round(a)), t*4, c, t, r, + vTherm, ptherm * 1000) + ofp.output(ostr) + + if finalTable: + ofp.output(" }") + else: + ofp.output(" },") + +def SteinhartHartTable(ofp, params, names, idx, settings, finalTable): + ofp.output((" // %s temp table using Steinhart-Hart algorithm with " + "parameters:") % (", ".join(names))) + ofp.output((" // Rp = %s, T0 = %s, R0 = %s, T1 = %s, R1 = %s, " + "T2 = %s, R2 = %s") % + (params[0], params[1], params[2], params[3], params[4], params[5], + params[6])) + ofp.output(" {") + + thrm = SHThermistor(int(params[0]), int(params[1]), int(params[2]), + int(params[3]), int(params[4]), int(params[5]), + int(params[6])) + + for t in idx: + a, r = thrm.setting(t) + if a is None: + ofp.output("// ERROR CALCULATING THERMISTOR VALUES AT TEMPERATURE %d" % t) + continue + + if t <= 0: + c = " " + else: + c = "," + ofp.output(" {%4d, %5d}%s // %4d C, %6.0f ohms, %7.2f adc" % + (int(round(a)), t*4, c, t, int(round(r)), a)) + + if finalTable: + ofp.output(" }") + else: + ofp.output(" },")