Add power meter.

pull/7/head
Peter Hinch 2017-11-03 12:11:31 +00:00
rodzic cc1e4076d6
commit 9f0ff308c4
13 zmienionych plików z 697 dodań i 8 usunięć

Wyświetl plik

@ -101,7 +101,7 @@ A means of rendering multiple larger fonts to the SSD1306 OLED display. See
[docs](./SSD1306/README.md).
# mutex
A class providing mutal exclusion enabling interrupt handlers and the main program to access shared
A class providing mutual exclusion enabling interrupt handlers and the main program to access shared
data in a manner which ensures data integrity.
# watchdog
@ -127,19 +127,20 @@ the same topic. Measures the round-trip delay. Adapt to suit your server address
QOS (quality of service, 0 and 1 are supported). After 100 messages reports maximum and
minimum delays.
conn.py Connect in station mode using saved connection details where possible
conn.py Connect in station mode using saved connection details where possible.
# Rotary Incremental Encoder
Classes for handling incremental rotary position encoders. Note that the Pyboard timers can
do this in hardware. These samples cater for cases where that solution can't be used. The
encoder_timed.py sample provides rate information by timing successive edges. In practice this
is likely to need filtering to reduce jitter caused by imperfections in the encoder geometry.
Classes for handling incremental rotary position encoders. Note that the Pyboard
timers can do this in hardware. These samples cater for cases where that
solution can't be used. The encoder_timed.py sample provides rate information by
timing successive edges. In practice this is likely to need filtering to reduce
jitter caused by imperfections in the encoder geometry.
There are other algorithms but this is the simplest and fastest I've encountered.
These were written for encoders producing TTL outputs. For switches, adapt the pull definition
to provide a pull up or pull down as required.
These were written for encoders producing TTL outputs. For switches, adapt the
pull definition to provide a pull up or pull down as required.
The `encoder.portable.py` version should work on all MicroPython platforms.
Tested on ESP8266. Note that interrupt latency on the ESP8266 limits performance
@ -159,6 +160,14 @@ of numbers following initialisation will always be the same.
See the code for usage and timing documentation.
# A design for a hardware power meter
This uses a Pyboard to measure the power consumption of mains powered devices.
Unlike simple commercial devices it performs a true vector (phasor) measurement
enabling it to provide information on power factor and to work with devices
which generate as well as consume power. It uses the official LCD160CR display
as a touch GUI interface. It is documented [here](./power/README.md).
# License
Any code placed here is released under the MIT License (MIT).

107
power/README.md 100644
Wyświetl plik

@ -0,0 +1,107 @@
# A phasor power meter
This measures the AC mains power consumed by a device. Unlike many cheap power
meters it performs a vector measurement and can display true power, VA and
phase. It can also plot snapshots of voltage and current waveforms. It can
calculate average power consumption of devices whose consumption varies with
time such as freezers and washing machines, and will work with devices capable
of sourcing power into the grid. It supports full scale ranges of 30W to 3KW.
[Images of device](./images/IMAGES.md)
## Warning
This project includes mains voltage wiring. Please don't attempt it unless you
have the necessary skills and experience to do this safely.
# Hardware Overview
The file `SignalConditioner.fzz` includes the schematic and PCB layout for the
device's input circuit. The Fritzing utility required to view and edit this is
available (free) from [here](http://fritzing.org/download/).
The unit includes a transformer with two 6VAC secondaries. One is used to power
the device, the other to measure the line voltage. Current is measured by means
of a current transformer SEN-11005 from SparkFun. The current output from this
is converted to a voltage by means of an op-amp configured as a transconductance
amplifier. This passes through a variable gain amplifier comprising two cascaded
MCP6S91 programmable gain amplifiers, then to a two pole Butterworth low pass
anti-aliasing filter. The resultant signal is presented to one of the Pyboard's
shielded ADC's. The transconductance amplifier also acts as a single pole low
pass filter.
The voltage signal is similarly filtered with three matched poles to ensure
that correct relative phase is maintained. The voltage channel has fixed gain.
## PCB
The PCB and schematic have an error in that the inputs of the half of opamp U4
which handles the current signal are transposed.
# Firmware Overview
## Dependencies
1. The `uasyncio` library.
2. The official lcd160 driver `lcd160cr.py`.
Also from the [lcd160cr GUI library](https://github.com/peterhinch/micropython-lcd160cr-gui.git)
the following files:
1. `lcd160_gui.py`.
2. `font10.py`.
3. `lcd_local.py`
4. `constants.py`
5. `lplot.py`
## Configuration
In my build the above plus `mains.py` are implemented as frozen bytecode. There
is no SD card, the flash filesystem containing `main.py` and `mt.py`.
If `mt.py` is deleted from flash and located on an SD card the code will create
simulated sinewave samples for testing.
## Design
The code has not been optimised for performance, which in my view is adequate
for the application.
The module `mains.py` contains two classes, `Preprocessor` and `Scaling` which
perform the data capture and analysis. The former acquires the data, normalises
it and calculates normalised values of RMS voltage and current along with power
and phase. `Scaling` controls the PGA according to the selected range and
modifies the Vrms, Irms and P values to be in conventional units.
The `Scaling` instance created in `mt.py` has a continuously running coroutine
(`._run()`) which reads a set of samples, processes them, and executes a
callback. Note that the callback function is changed at runtime by the GUI code
(by `mains_device.set_callback()`). The iteration rate of `._run()` is about
1Hz.
The code is intended to offer a degree of noise immunity, in particular in the
detection of voltage zero crossings. It operates by acquiring a set of 400
sample pairs (voltage and current) as fast as standard MicroPython can achieve.
On the Pyboard with 50Hz mains this captures two full cycles, so guaranteeing
two positive going voltage zero crossings. The code uses an averaging algorithm
to detect these (`Preprocessor.analyse()`) and populates four arrays of floats
with precisely one complete cycle of data. The arrays comprise two pairs of
current and voltage values, one scaled for plotting and the other scaled for
measurement.
Both pairs are scaled to a range of +-1.0 with any DC bias removed (owing to
the presence of transformers this can only arise due to offsets in the
circuitry and/or ADC's). DC removal facilitates long term integration.
Plot data is further normalised so that current values exactly fill the +-1.0
range. In other words plots are scaled so that the current waveform fills the
Y axis with the X axis containing one full cycle. The voltage plot is made 10%
smaller to avoid the visually confusing situation with a resistive load where
the two plots coincide exactly.
## Calibration
This is defined by `Scaling.vscale` and `Scaling.iscale`. These values were
initially calculated, then adjusted by comparing voltage and current readings
with measurements from a calibrated meter. Voltage calibration in particular
will probably need adjusting depending on the transformer characteristics.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,23 @@
# Sample Images
#Puhbuttons
![Exterior](./outside.JPG)
![Interior](./interior.JPG)
Interior construction.
![Microwave](./microwave.JPG)
Microwave oven.
![Plot](./plot.JPG)
Microwave waveforms.
![Integrate](./integrate.JPG)
Integration screen.
![Underrange](./underrange.JPG)
Soldering iron on 3KW range.
![Correct range](./correctrange.JPG)
Same iron on 30W range

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 36 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 36 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 361 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 37 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 164 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 34 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 37 KiB

326
power/mains.py 100644
Wyświetl plik

@ -0,0 +1,326 @@
# mains.py Data collection and processing module for the power meter.
# The MIT License (MIT)
#
# Copyright (c) 2017 Peter Hinch
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from pyb import SPI, Pin, ADC
from array import array
from micropython import const
from math import sin, cos, asin, pi, radians, sqrt
import uasyncio as asyncio
import gc
gc.collect()
# PINOUT
# vadc X19
# iadc X20
# MOSI X8
# MISO X7
# SCK X6
# CSN0 X5 First PGA
# CSN1 X4 Second PGA
# SAMPLING
NSAMPLES = const(400)
def arr_gen(n):
for _ in range(n):
yield 0
isamples = array('h', arr_gen(NSAMPLES))
vsamples = array('h', arr_gen(NSAMPLES))
gc.collect()
# HARDWARE
vadc = ADC(Pin.board.X19)
iadc = ADC(Pin.board.X20)
spi = SPI(1)
spi.init(SPI.MASTER, polarity = 0, phase = 0)
# ************* Programmable Gain Amplifier *************
class MCP6S91():
CHANNEL_ADDR = 0x41
GAIN_ADDR = 0x40
GAINVALS = (1, 2, 4, 5, 8, 10, 16, 32)
PINS = ('X5', 'X4')
def __init__(self, devno):
try:
self.csn = Pin(MCP6S91.PINS[devno], mode = Pin.OUT_PP)
except IndexError:
raise ValueError('MCP6S91 device no. must be 0 or 1.')
self.csn.value(1)
self.csn.value(0)
self.csn.value(1) # Set state machine to known state
self.gain(1)
def gain(self, value):
try:
gainval = MCP6S91.GAINVALS.index(value)
except ValueError:
raise ValueError('MCP6S91 invalid gain {}'.format(value))
self.csn.value(0)
spi.send(MCP6S91.GAIN_ADDR)
spi.send(gainval)
self.csn.value(1)
# Two cascaded MCP6S91 devices provide gains 1, 2, 5, 10, 20, 50, 100
class PGA():
gvals = { 1 : (1, 1), 2 : (2, 1), 5 : (5, 1), 10 : (10, 1),
20 : (10, 2), 50 : (10, 5), 100 : (10, 10) }
def __init__(self):
self.amp0 = MCP6S91(0)
self.amp1 = MCP6S91(1)
def gain(self, value):
try:
g0, g1 = PGA.gvals[value]
except KeyError:
raise ValueError('PGA invalid gain {}'.format(value))
self.amp0.gain(g0)
self.amp1.gain(g1)
# ************* Data Acquisition *************
# Integer sample data put into vsamples and isamples global arrays.
# Results are in range +-2047
# SIMULATION PARAMETERS
VPEAK = 0.545 # Relative to ADC FSD
IPEAK = 0.6
VPHASE = 0
IPHASE = radians(45)
def sample(simulate=False):
# Acquire just over 2 full cycles of AC
if simulate:
for n in range(NSAMPLES):
isamples[n] = int(2047 + IPEAK * 2047 * sin(4.2 * pi * n / NSAMPLES + IPHASE))
vsamples[n] = int(2047 + VPEAK * 2047 * sin(4.2 * pi * n / NSAMPLES + VPHASE))
else:
for n in range(NSAMPLES):
isamples[n] = iadc.read()
vsamples[n] = vadc.read()
for n in range(NSAMPLES): # Normalise to -2047 to +2048
vsamples[n] -= 2047
if simulate:
isamples[n] -= 2047
else:
isamples[n] = 2047 - isamples[n] # Sod's law. That's the way I wired the CT.
# ************* Preprocessing *************
# Filter data. Produce a single cycle of floating point data in two datasets.
# Both are scaled -1.0 to +1.0.
# Plot data is scaled such that the data exactly fits the range.
# Output data is scaled such that DAC FS fits the range.
class Preprocessor():
arraysize = const(NSAMPLES // 2) # We acquire > 2 cycles
plotsize = const(50)
vplot = array('f', arr_gen(plotsize)) # Plot data
iplot = array('f', arr_gen(plotsize))
vscale = array('f', arr_gen(arraysize)) # Output data
iscale = array('f', arr_gen(arraysize))
def __init__(self, simulate, verbose):
self.avg_len = 4
self.avg_half = self.avg_len // 2
gc.collect()
self.simulate = simulate
self.verbose = verbose
self.overrange = False
self.threshold = 1997
def vprint(self, *args):
if self.verbose:
print(*args)
async def run(self):
self.overrange = False
sample(self.simulate)
return await self.analyse()
# Calculate average of avg_len + 1 numbers around a centre value. avg_len must be divisible by 2.
# This guarantees symmetry around the centre index.
def avg(self, arr, centre):
return sum(arr[centre - self.avg_half : centre + 1 + self.avg_half]) / (self.avg_len + 1)
# Filter a set of samples around a centre index in an array
def filt(self, arr, centre):
avg0 = self.avg(arr, centre - self.avg_half)
avg1 = self.avg(arr, centre + self.avg_half)
return avg0, avg1
async def analyse(self):
# Determine the first and second rising edge of voltage
self.overrange = False
nfirst = -1 # Index of 1st upward voltage transition
lastv = 0 # previous max
ovr = self.threshold # Overrange threshold
for n in range(self.avg_len, NSAMPLES - self.avg_len + 1):
vavg0, vavg1 = self.filt(vsamples, n)
iavg0, iavg1 = self.filt(isamples, n)
vmax = max(vavg0, vavg1)
vmin = min(vavg0, vavg1)
imax = max(iavg0, iavg1)
imin = min(iavg0, iavg1)
if vmax > ovr or vmin < -ovr or imax > ovr or imin < -ovr:
self.overrange = True
self.vprint('overrange', vmax, vmin, imax, imin)
if nfirst == -1:
if vavg0 < 0 and vavg1 > 0 and abs(vmin) < lastv:
nfirst = n if err > abs(abs(vmin) - lastv) else n - 1
irising = iavg0 < iavg1 # Save current rising state for phase calculation
elif n > nfirst + NSAMPLES // 6:
if vavg0 < 0 and vavg1 > 0 and abs(vmin) < lastv:
nsecond = n if err > abs(abs(vmin) - lastv) else n - 1
break
lastv = vmax
err = abs(abs(vmin) - lastv)
yield
else: # Should never occur because voltage should be present.
raise OSError('Failed to find a complete cycle.')
self.vprint(nfirst, nsecond, vsamples[nfirst], vsamples[nsecond], isamples[nfirst], isamples[nsecond])
# Produce datasets for a single cycle of V.
# Scale ADC FSD [-FSD 0 FSD] -> [-1.0 0 +1.0]
nelems = nsecond - nfirst + 1 # No. of samples in current cycle
p = 0
for n in range(nfirst, nsecond + 1):
self.vscale[p] = vsamples[n] / 2047
self.iscale[p] = isamples[n] / 2047
p += 1
# Remove DC offsets
sumv = 0.0
sumi = 0.0
for p in range(nelems):
sumv += self.vscale[p]
sumi += self.iscale[p]
meanv = sumv / nelems
meani = sumi / nelems
maxv = 0.0
maxi = 0.0
yield
for p in range(nelems):
self.vscale[p] -= meanv
self.iscale[p] -= meani
maxv = max(maxv, abs(self.vscale[p])) # Scaling for plot
maxi = max(maxi, abs(self.iscale[p]))
yield
# Produce plot datasets. vplot scaled to avoid exact overlay of iplot
maxv = max(maxv * 1.1, 0.01) # Cope with "no signal" conditions
maxi = max(maxi, 0.01)
offs = 0
delta = nelems / (self.plotsize -1)
for p in range(self.plotsize):
idx = min(round(offs), nelems -1)
self.vplot[p] = self.vscale[idx] / maxv
self.iplot[p] = self.iscale[idx] / maxi
offs += delta
if self.verbose:
for p in range(nelems):
print('{:7.3f} {:7.3f} {:7.3f} {:7.3f}'.format(
self.vscale[p], self.iscale[p],
self.vplot[round(p / delta)], self.iplot[round(p / delta)]))
phase = asin(self.iplot[0]) if irising else pi - asin(self.iplot[0])
yield
# calculate power, vrms, irms etc prior to scaling
us_vrms = 0
us_pwr = 0
us_irms = 0
for p in range(nelems):
us_vrms += self.vscale[p] * self.vscale[p] # More noise-immune than calcuating from vmax * sqrt(2)
us_irms += self.iscale[p] * self.iscale[p]
us_pwr += self.iscale[p] * self.vscale[p]
us_vrms = sqrt(us_vrms / nelems)
us_irms = sqrt(us_irms / nelems)
us_pwr /= nelems
return phase, us_vrms, us_irms, us_pwr, nelems
# Testing. Current provided to CT by 100ohm in series with secondary. Vsec = 7.8Vrms so i = 78mArms == 18W at 230Vrms.
# Calculated current scaling:
# FSD 3.3Vpp out = 1.167Vrms.
# Secondary current Isec = 1.167/180ohm = 6.5mArms.
# Ratio = 2000. Primary current = 12.96Arms.
# At FSD iscale is +-1 = 0.707rms
# Imeasured = us_irms * 12.96/(0.707 * self.pga_gain) = us_irms * 18.331 / self.pga_gain
# Voltage scaling. Measured ADC I/P = 1.8Vpp == 0.545fsd pp == 0.386fsd rms
# so vscale = 230/0.386 = 596
class Scaling():
# FS Watts to PGA gain
valid_gains = (3000, 1500, 600, 300, 150, 60, 30)
vscale = 679 # These were re-measured with the new build. Zero load.
iscale = 18.0 # Based on measurement with 2.5KW resistive load (kettle)
def __init__(self, simulate=False, verbose=False):
self.cb = None
self.preprocessor = Preprocessor(simulate, verbose)
self.pga = PGA()
self.set_range(3000)
loop = asyncio.get_event_loop()
loop.create_task(self._run())
async def _run(self):
while True:
if self.cb is not None:
phase, us_vrms, us_irms, us_pwr, nelems = await self.preprocessor.run() # Get unscaled values. Takes 360ms.
if self.cb is not None: # May have changed during await
vrms = us_vrms * self.vscale
irms = us_irms * self.iscale / self.pga_gain
pwr = us_pwr * self.iscale * self.vscale / self.pga_gain
self.cb(phase, vrms, irms, pwr, nelems, self.preprocessor.overrange)
yield
def set_callback(self, cb=None):
self.cb = cb # Set to None to pause acquisition
def set_range(self, val):
if val in self.valid_gains:
self.pga_gain = 3000 // val
self.pga.gain(self.pga_gain)
else:
raise ValueError('Invalid range. Valid ranges (W) {}'.format(self.valid_gains))
@property
def vplot(self):
return self.preprocessor.vplot
@property
def iplot(self):
return self.preprocessor.iplot
def test():
def cb(phase, vrms, irms, pwr, nelems, ovr):
print('Phase {:5.1f}rad {:5.0f}Vrms {:6.3f}Arms {:6.1f}W Nsamples = {:3d}'.format(phase, vrms, irms, pwr, nelems))
if ovr:
print('Overrange')
s = Scaling(True, True)
s.set_range(300)
s.set_callback(cb)
loop = asyncio.get_event_loop()
loop.run_forever()

224
power/mt.py 100644
Wyświetl plik

@ -0,0 +1,224 @@
# mt.py Display module for the power meter
# The MIT License (MIT)
#
# Copyright (c) 2017 Peter Hinch
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import uasyncio as asyncio
from mains import Scaling
from constants import *
from lcd160_gui import Button, Label, Screen, Dropdown, Dial, LED, ButtonList
from lplot import CartesianGraph, Curve
import font10
from lcd_local import setup
from utime import ticks_ms, ticks_diff
from os import listdir
if 'mt.py' in listdir('/flash'):
mains_device = Scaling() # Real
# mains_device = Scaling(True, False) # Simulate
else: # Running on SD card - test setup
mains_device = Scaling(True, False) # Simulate
# STANDARD BUTTONS
def fwdbutton(y, screen, text, color):
def fwd(button, screen):
Screen.change(screen)
return Button((109, y), font = font10, fontcolor = BLACK, callback = fwd,
args = [screen], fgcolor = color, text = text)
def backbutton():
def back(button):
Screen.back()
return Button((139, 0), font = font10, fontcolor = BLACK, callback = back,
fgcolor = RED, text = 'X', height = 20, width = 20)
def plotbutton(y, screen, color):
def fwd(button, screen):
Screen.change(screen)
return Button((139, y), font = font10, fontcolor = BLACK, callback = fwd,
args = [screen], fgcolor = color, text = '~', height = 20, width = 20)
# **** BASE SCREEN ****
class BaseScreen(Screen):
def __init__(self):
super().__init__()
self.pwr_range = 3000
# Buttons
fwdbutton(57, IntegScreen, 'Integ', CYAN)
fwdbutton(82, PlotScreen, 'Plot', YELLOW)
# Labels
self.lbl_pf = Label((0, 31), font = font10, width = 75, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_v = Label((0, 56), font = font10, width = 75, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_i = Label((0, 81), font = font10, width = 75, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_p = Label((0,106), font = font10, width = 75, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_va = Label((80,106), font = font10, width = 79, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
# Dial
self.dial = Dial((109, 0), fgcolor = YELLOW, border = 2)
# Dropdown
self.dropdown = Dropdown((0, 0), font = font10, width = 80, callback = self.cbdb,
elements = ('3000W', '600W', '150W', '60W', '30W'))
self.led = LED((84, 0), color = GREEN)
self.led.value(True)
# Dropdown callback: set range
def cbdb(self, dropdown):
self.pwr_range = int(dropdown.textvalue()[: -1]) # String of form 'nnnW'
mains_device.set_range(self.pwr_range)
# print('Range set to', self.pwr_range, dropdown.value())
def reading(self, phase, vrms, irms, pwr, nelems, ovr):
# print(phase, vrms, irms, pwr, nelems)
self.lbl_v.value('{:5.1f}V'.format(vrms))
if ovr:
self.lbl_i.value('----')
self.lbl_p.value('----')
self.lbl_pf.value('----')
self.lbl_va.value('----')
else:
self.lbl_i.value('{:6.3f}A'.format(irms))
self.lbl_p.value('{:5.1f}W'.format(pwr))
self.lbl_pf.value('PF:{:4.2f}'.format(pwr /(vrms * irms)))
self.lbl_va.value('{:5.1f}VA'.format(vrms * irms))
self.dial.value(phase + 1.5708) # Conventional phasor orientation.
if ovr:
self.led.color(RED) # Overrange
elif abs(pwr) < abs(self.pwr_range) / 5:
self.led.color(YELLOW) # Underrange
else:
self.led.color(GREEN) # OK
def on_hide(self):
mains_device.set_callback(None) # Stop readings
def after_open(self):
mains_device.set_callback(self.reading)
# **** PLOT SCREEN ****
class PlotScreen(Screen):
@staticmethod
def populate(curve, data):
xinc = 1 / len(data)
x = 0
for y in data:
curve.point(x, y)
x += xinc
def __init__(self):
super().__init__()
backbutton()
Label((142, 45), font = font10, fontcolor = YELLOW, value = 'V')
Label((145, 70), font = font10, fontcolor = RED, value = 'I')
g = CartesianGraph((0, 0), height = 127, width = 135, xorigin = 0) # x >= 0
Curve(g, self.populate, args = (mains_device.vplot,))
Curve(g, self.populate, args = (mains_device.iplot,), color = RED)
# **** INTEGRATOR SCREEN ****
class IntegScreen(Screen):
def __init__(self):
super().__init__()
# Buttons
backbutton()
plotbutton(80, PlotScreen, YELLOW)
# Labels
self.lbl_p = Label((0, 0), font = font10, width = 78, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
Label((90, 4), font = font10, value = 'Power')
self.lbl_pmax = Label((0, 30), font = font10, width = 78, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
Label((90, 34), font = font10, value = 'Max')
self.lbl_pin = Label((0, 55), font = font10, width = 78, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_pmin = Label((0, 80), font = font10, width = 78, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
Label((90, 84), font = font10, value = 'Min')
self.lbl_w_hr = Label((0,105), font = font10, width = 78, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
self.lbl_t = Label((88, 105), font = font10, width = 70, border = 2, bgcolor = DARKGREEN, fgcolor = RED)
table = [
{'fgcolor' : GREEN, 'text' : 'Max Gen', 'args' : (True,)},
{'fgcolor' : BLUE, 'text' : 'Mean', 'args' : (False,)},
]
bl = ButtonList(self.buttonlist_cb)
for t in table: # Buttons overlay each other at same location
bl.add_button((90, 56), width = 70, font = font10, fontcolor = BLACK, **t)
self.showmean = False
self.t_reading = None # Time of last reading
self.t_start = None # Time of 1st reading
self.joules = 0 # Cumulative energy
self.overrange = False
self.wmax = 0 # Max power out
self.wmin = 0 # Max power in
self.pwr_min = 10000 # Power corresponding to minimum absolute value
def reading(self, phase, vrms, irms, pwr, nelems, ovr):
self.wmax = max(self.wmax, pwr)
self.wmin = min(self.wmin, pwr)
if abs(pwr) < abs(self.pwr_min):
self.pwr_min = pwr
if ovr:
self.overrange = True
t_last = self.t_reading # Time of last reading (ms)
self.t_reading = ticks_ms()
if self.t_start is None: # 1st reading
self.t_start = self.t_reading # Time of 1st reading
else:
self.joules += pwr * ticks_diff(self.t_reading, t_last) / 1000
secs_since_start = ticks_diff(self.t_reading, self.t_start) / 1000 # Runtime
mins, secs = divmod(int(secs_since_start), 60)
hrs, mins = divmod(mins, 60)
self.lbl_t.value('{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs))
if ovr:
self.lbl_p.value('----')
else:
self.lbl_p.value('{:5.1f}W'.format(pwr))
if self.showmean:
self.lbl_pin.value('{:5.1f}W'.format(self.joules / max(secs_since_start, 1)))
else:
self.lbl_pin.value('{:5.1f}W'.format(self.wmin))
self.lbl_pmin.value('{:5.1f}W'.format(self.pwr_min))
if self.overrange: # An overrange occurred during the measurement
self.lbl_w_hr.value('----')
self.lbl_pmax.value('----')
else:
self.lbl_pmax.value('{:5.1f}W'.format(self.wmax))
units = self.joules / 3600
if units < 1000:
self.lbl_w_hr.value('{:6.0f}Wh'.format(units))
else:
self.lbl_w_hr.value('{:6.2f}KWh'.format(units / 1000))
def buttonlist_cb(self, button, arg):
self.showmean = arg
def on_hide(self):
mains_device.set_callback(None) # Stop readings
def after_open(self):
mains_device.set_callback(self.reading)
def test():
print('Running...')
setup()
Screen.change(BaseScreen)
test()