9 Commits

Author SHA1 Message Date
Nicholas Thompson
574e832152 Merge pull request #32 from diebietse/fix_frequency
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
Add frequency clip check
2022-01-26 20:22:17 +02:00
Nicholas Thompson
2a84799832 Add frequency clip check
Add check to ensure frequency value is set to 0 when period counter hits maximum or minimum
Closes #31
2022-01-26 20:11:15 +02:00
Nicholas Thompson
8adf3c8261 Merge pull request #29 from diebietse/fix-signedness
No longer use RAM value scale signedness for info frames.
2022-01-25 10:32:56 +02:00
Hendrik van Wyk
b3245aba9b No longer use RAM value scale signedness for info frames.
The signedness calculated along with the RAM value scale and offset was
incorrectly applied to the info frame value. This caused some values to
be interpreted as unsigned instead of signed leading to negative values
reporting as very large positive values.
2022-01-11 20:36:56 +02:00
Nicholas Thompson
dac2149fbd Merge pull request #28 from Banshee1221/master
github ci: add arm v6 docker image builds
2021-12-09 12:30:55 +02:00
Eugene de Beste
e501f6d125 github ci: add arm v6 docker image builds 2021-12-09 09:09:11 +02:00
Jedri Visser
341f26f197 Merge pull request #26 from diebietse/update
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
Update Dockerfile, vuejs and bootstrap
2021-05-07 23:30:55 +02:00
Jedri Visser
5e28643a76 Update vuejs and bootstrap 2021-05-07 23:25:07 +02:00
Jedri Visser
b35132451c Clean up Dockerfile 2021-05-07 22:57:45 +02:00
7 changed files with 257 additions and 181 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@@ -1,14 +1,14 @@
FROM golang:alpine as builder
RUN apk add git
FROM golang:1.16-alpine as builder
RUN mkdir /build
COPY . /build/
WORKDIR /build
RUN go build -o invertergui ./cmd/invertergui
FROM alpine
RUN adduser -S -D -H -h /app inverteruser
RUN addgroup inverteruser dialout
USER inverteruser
COPY --from=builder /build/invertergui /app/
WORKDIR /app
ENTRYPOINT [ "./invertergui" ]
CMD []
RUN CGO_ENABLED=0 go build -o invertergui ./cmd/invertergui
FROM scratch
# Group ID 20 is dialout, needed for tty read/write access
USER 3000:20
COPY --from=builder /build/invertergui /bin/
ENTRYPOINT [ "/bin/invertergui" ]
EXPOSE 8080

View File

@@ -49,7 +49,7 @@ const (
acL1InfoFrame = 0x08
dcInfoFrame = 0x0C
setTargetFrame = 0x41
infoReqFrame = 0x46
infoReqFrame = 0x46 //F
ledFrame = 0x4C
vFrame = 0x56
winmonFrame = 0x57
@@ -346,7 +346,7 @@ func (m *mk2Ser) dcDecode(frame []byte) {
chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat)
m.info.BatCurrent = usedC - chargeC
m.info.OutFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarInverterPeriod))
m.info.OutFrequency = m.calcFreq(frame[13], ramVarInverterPeriod)
logrus.Debugf("dcDecode %#v", m.info)
// Send L1 status request
@@ -358,16 +358,12 @@ func (m *mk2Ser) dcDecode(frame []byte) {
// Decodes AC frame.
func (m *mk2Ser) acDecode(frame []byte) {
m.info.InVoltage = m.applyScaleAndSign(frame[5:7], ramVarVMains)
m.info.InCurrent = m.applyScaleAndSign(frame[7:9], ramVarIMains)
m.info.OutVoltage = m.applyScaleAndSign(frame[9:11], ramVarVInverter)
m.info.OutCurrent = m.applyScaleAndSign(frame[11:13], ramVarIInverter)
m.info.InVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVMains)
m.info.InCurrent = m.applyScale(getSigned(frame[7:9]), ramVarIMains)
m.info.OutVoltage = m.applyScale(getSigned(frame[9:11]), ramVarVInverter)
m.info.OutCurrent = m.applyScale(getSigned(frame[11:13]), ramVarIInverter)
m.info.InFrequency = m.calcFreq(frame[13], ramVarMainPeriod)
if frame[13] == 0xff {
m.info.InFrequency = 0
} else {
m.info.InFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarMainPeriod))
}
logrus.Debugf("acDecode %#v", m.info)
// Send status request
@@ -376,6 +372,13 @@ func (m *mk2Ser) acDecode(frame []byte) {
m.sendCommand(cmd)
}
func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
if data == 0xff || data == 0x00 {
return 0
}
return 10 / (m.applyScale(float64(data), scaleIndex))
}
// Decode charge state of battery.
func (m *mk2Ser) stateDecode(frame []byte) {
m.info.ChargeState = m.applyScaleAndSign(frame[1:3], ramVarChargeState)

View File

@@ -33,7 +33,7 @@ var knownWrites = []byte{
var writeBuffer = bytes.NewBuffer(nil)
const (
testEpsilon = 0.00000001
testDelta = 0.00000001
)
type testIo struct {
@@ -50,9 +50,18 @@ func NewIOStub(readBuffer []byte) io.ReadWriter {
// Test a know sequence as reference as extracted from Mk2
func TestSync(t *testing.T) {
knownReadBuffer := []byte{
tests := []struct {
name string
knownReadBuffer []byte
knownWrites []byte
result Mk2Info
}{
{
name: "basic",
knownReadBuffer: []byte{
//Len Cmd
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb, 0x07, 0xff, 0x56, 0x96, 0x3e, 0x11, 0x00, 0x00, 0xbf,
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x07, 0xff, 0x56, 0x96, 0x3e, 0x11, 0x00, 0x00, 0xbf,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x00, 0x00, 0xa1,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
@@ -72,9 +81,40 @@ func TestSync(t *testing.T) {
0x0f, 0x20, 0x01, 0x01, 0xca, 0x09, 0x08, 0xaa, 0x58, 0xab, 0x00, 0xaa, 0x58, 0x9a, 0x00, 0xc3, 0xe8,
0x06, 0xff, 0x4c, 0x03, 0x00, 0x00, 0x00, 0xac,
0x05, 0xff, 0x57, 0x85, 0xc8, 0x00, 0x58,
}
expectedLEDs := map[Led]LEDstate{
},
knownWrites: []byte{
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x05, 0xff, 0x57, 0x36, 0x00, 0x00, 0x6f,
0x05, 0xff, 0x57, 0x36, 0x01, 0x00, 0x6e,
0x05, 0xff, 0x57, 0x36, 0x02, 0x00, 0x6d,
0x05, 0xff, 0x57, 0x36, 0x03, 0x00, 0x6c,
0x05, 0xff, 0x57, 0x36, 0x04, 0x00, 0x6b,
0x05, 0xff, 0x57, 0x36, 0x05, 0x00, 0x6a,
0x05, 0xff, 0x57, 0x36, 0x06, 0x00, 0x69,
0x05, 0xff, 0x57, 0x36, 0x07, 0x00, 0x68,
0x05, 0xff, 0x57, 0x36, 0x08, 0x00, 0x67,
0x05, 0xff, 0x57, 0x36, 0x09, 0x00, 0x66,
0x05, 0xff, 0x57, 0x36, 0x0a, 0x00, 0x65,
0x05, 0xff, 0x57, 0x36, 0x0b, 0x00, 0x64,
0x05, 0xff, 0x57, 0x36, 0x0c, 0x00, 0x63,
0x05, 0xff, 0x57, 0x36, 0x0d, 0x00, 0x62,
0x03, 0xff, 0x46, 0x00, 0xb8,
0x03, 0xff, 0x46, 0x01, 0xb7,
0x02, 0xff, 0x4c, 0xb3,
0x05, 0xff, 0x57, 0x30, 0x0d, 0x00, 0x68,
},
result: Mk2Info{
Version: uint32(2736),
BatVoltage: 14.41,
BatCurrent: -0.4,
InVoltage: 226.98,
InCurrent: 1.71,
InFrequency: 50.10256410256411,
OutVoltage: 226.980,
OutCurrent: 1.54,
OutFrequency: 50.025510204081634,
ChargeState: 1,
LEDs: map[Led]LEDstate{
LedMain: LedOn,
LedAbsorption: LedOn,
LedBulk: LedOff,
@@ -83,29 +123,89 @@ func TestSync(t *testing.T) {
LedOverload: LedOff,
LedLowBattery: LedOff,
LedTemperature: LedOff,
},
},
},
{
name: "multiplus24/3000",
knownReadBuffer: []byte{
//Len Cmd
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x07, 0xff, 0x56, 0x98, 0x3e, 0x11, 0x00, 0x00, 0xbd, // version
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 0
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 1
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 2
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 3
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 4
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x0, 0x0, 0xa1, // scale 5
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 6
0x08, 0xff, 0x57, 0x8e, 0x57, 0x78, 0x8f, 0x0, 0x1, 0xb5, // scale 7
0x08, 0xff, 0x57, 0x8e, 0x2f, 0x7c, 0x8f, 0x0, 0x0, 0xda, // scale 8
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x0, 0x0, 0xa1, //scale 9
0x08, 0xff, 0x57, 0x8e, 0x4, 0x0, 0x8f, 0x0, 0x80, 0x1, // scale 10
0x08, 0xff, 0x57, 0x8e, 0x1, 0x0, 0x8f, 0x0, 0x80, 0x4, // scale 11
0x08, 0xff, 0x57, 0x8e, 0x6, 0x0, 0x8f, 0x0, 0x80, 0xff, // scale 12
0x08, 0xff, 0x57, 0x8e, 0x38, 0x7f, 0x8f, 0x0, 0x0, 0xce, // scale 13
0x07, 0xff, 0x56, 0x98, 0x3e, 0x11, 0x0, 0x0, 0xbd, // version
0x0f, 0x20, 0xb6, 0x89, 0x6d, 0xb7, 0xc, 0x4e, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x88, 0x82, // dc info
0x0f, 0x20, 0x1, 0x1, 0x6d, 0xb7, 0x8, 0x77, 0x5b, 0x21, 0x0, 0x77, 0x5b, 0xfe, 0xff, 0xc3, 0x1e, // ac info
0x08, 0xff, 0x4c, 0x9, 0x0, 0x0, 0x0, 0x3, 0x0, 0xa1,
0x05, 0xff, 0x57, 0x85, 0xc8, 0x0, 0x58,
},
knownWrites: []byte{},
result: Mk2Info{
Version: 0xac0,
BatVoltage: 26.38,
BatCurrent: 0,
InVoltage: 234.15,
InCurrent: 0.33,
InFrequency: 50.1025641025641,
OutVoltage: 234.15,
OutCurrent: -0.02,
OutFrequency: 50.025510204081634,
ChargeState: 1,
LEDs: map[Led]LEDstate{
LedMain: LedOn,
LedAbsorption: LedOff,
LedBulk: LedOff,
LedFloat: LedOn,
LedInverter: LedOff,
LedOverload: LedOff,
LedLowBattery: LedOff,
LedTemperature: LedOff,
},
},
},
}
testIO := NewIOStub(knownReadBuffer)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testIO := NewIOStub(tt.knownReadBuffer)
mk2, err := NewMk2Connection(testIO)
assert.NoError(t, err, "Could not open MK2")
event := <-mk2.C()
mk2.Close()
if len(tt.knownWrites) > 0 {
assert.Equal(t, 0, bytes.Compare(writeBuffer.Bytes(), knownWrites), "Expected writes did not match received writes")
}
assert.True(t, event.Valid, "data not valid")
assert.Equal(t, uint32(2736), event.Version, "Invalid version decoded")
assert.Equal(t, tt.result.Version, event.Version, "Invalid version decoded")
assert.Equal(t, 0, len(event.Errors), "Reported errors not empty")
assert.Equal(t, expectedLEDs, event.LEDs, "Reported LEDs incorrect")
assert.Equal(t, tt.result.LEDs, event.LEDs, "Reported LEDs incorrect")
assert.InEpsilon(t, 14.41, event.BatVoltage, testEpsilon, "BatVoltage conversion failed")
assert.InEpsilon(t, -0.4, event.BatCurrent, testEpsilon, "BatCurrent conversion failed")
assert.InEpsilon(t, 226.98, event.InVoltage, testEpsilon, "InVoltage conversion failed")
assert.InEpsilon(t, 1.71, event.InCurrent, testEpsilon, "InCurrent conversion failed")
assert.InEpsilon(t, 50.10256410256411, event.InFrequency, testEpsilon, "InFrequency conversion failed")
assert.InEpsilon(t, 226.980, event.OutVoltage, testEpsilon, "OutVoltage conversion failed")
assert.InEpsilon(t, 1.54, event.OutCurrent, testEpsilon, "OutCurrent conversion failed")
assert.InEpsilon(t, 50.025510204081634, event.OutFrequency, testEpsilon, "OutFrequency conversion failed")
assert.InEpsilon(t, 1, event.ChargeState, testEpsilon, "ChargeState conversion failed")
assert.InDelta(t, tt.result.BatVoltage, event.BatVoltage, testDelta, "BatVoltage conversion failed")
assert.InDelta(t, tt.result.BatCurrent, event.BatCurrent, testDelta, "BatCurrent conversion failed")
assert.InDelta(t, tt.result.InVoltage, event.InVoltage, testDelta, "InVoltage conversion failed")
assert.InDelta(t, tt.result.InCurrent, event.InCurrent, testDelta, "InCurrent conversion failed")
assert.InDelta(t, tt.result.InFrequency, event.InFrequency, testDelta, "InFrequency conversion failed")
assert.InDelta(t, tt.result.OutVoltage, event.OutVoltage, testDelta, "OutVoltage conversion failed")
assert.InDelta(t, tt.result.OutCurrent, event.OutCurrent, testDelta, "OutCurrent conversion failed")
assert.InDelta(t, tt.result.OutFrequency, event.OutFrequency, testDelta, "OutFrequency conversion failed")
assert.InDelta(t, tt.result.ChargeState, event.ChargeState, testDelta, "ChargeState conversion failed")
})
}
}
func Test_mk2Ser_scaleDecode(t *testing.T) {
@@ -116,16 +216,16 @@ func Test_mk2Ser_scaleDecode(t *testing.T) {
}{
{
name: "Valid scale",
frame: []byte{0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a},
frame: []byte{0x8e, 0x9c, 0x7f, 0x8f, 0x01, 0x00, 0x6a},
expectedScaling: scaling{
scale: 0.00013679890560875513,
offset: 143,
scale: 0.01,
offset: 1,
supported: true,
},
},
{
name: "Unsupported frame",
frame: []byte{0x57, 0x00},
frame: []byte{0x00},
expectedScaling: scaling{
supported: false,
},
@@ -141,10 +241,71 @@ func Test_mk2Ser_scaleDecode(t *testing.T) {
assert.Equal(t, 1, len(m.scales))
assert.Equal(t, 1, m.scaleCount)
assert.Equal(t, tt.expectedScaling.supported, m.scales[0].supported)
assert.Equal(t, tt.expectedScaling.signed, m.scales[0].signed)
if tt.expectedScaling.supported {
assert.InEpsilon(t, tt.expectedScaling.offset, m.scales[0].offset, testEpsilon)
assert.InEpsilon(t, tt.expectedScaling.scale, m.scales[0].scale, testEpsilon)
assert.InDelta(t, tt.expectedScaling.offset, m.scales[0].offset, testDelta)
assert.InDelta(t, tt.expectedScaling.scale, m.scales[0].scale, testDelta)
}
})
}
}
func Test_mk2Ser_calcFreq(t *testing.T) {
tests := []struct {
name string
scales []scaling
data byte
scaleIndex int
want float64
}{
{
name: "Calculate working low",
scales: []scaling{
{supported: false},
},
data: 0x01,
scaleIndex: 0,
want: 10,
},
{
name: "Calculate working high",
scales: []scaling{
{
supported: true,
offset: 0,
scale: 0.01,
},
},
data: 0xFE,
scaleIndex: 0,
want: 3.937007874015748,
},
{
name: "Calculate clip high",
scales: []scaling{
{supported: false},
},
data: 0xff,
scaleIndex: 0,
want: 0,
},
{
name: "Calculate clip low",
scales: []scaling{
{supported: false},
},
data: 0x00,
scaleIndex: 0,
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &mk2Ser{
scales: tt.scales,
}
got := m.calcFreq(tt.data, tt.scaleIndex)
assert.InDelta(t, tt.want, got, testDelta)
})
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'">
<title>Page not found &middot; GitHub Pages</title>
<style type="text/css" media="screen">
body {
background-color: #f1f1f1;
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.container { margin: 50px auto 40px auto; width: 600px; text-align: center; }
a { color: #4183c4; text-decoration: none; }
a:hover { text-decoration: underline; }
h1 { width: 800px; position:relative; left: -100px; letter-spacing: -1px; line-height: 60px; font-size: 60px; font-weight: 100; margin: 0px 0 50px 0; text-shadow: 0 1px 0 #fff; }
p { color: rgba(0, 0, 0, 0.5); margin: 20px 0; line-height: 1.6; }
ul { list-style: none; margin: 25px 0; padding: 0; }
li { display: table-cell; font-weight: bold; width: 1%; }
.logo { display: inline-block; margin-top: 35px; }
.logo-img-2x { display: none; }
@media
only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and ( min--moz-device-pixel-ratio: 2),
only screen and ( -o-min-device-pixel-ratio: 2/1),
only screen and ( min-device-pixel-ratio: 2),
only screen and ( min-resolution: 192dpi),
only screen and ( min-resolution: 2dppx) {
.logo-img-1x { display: none; }
.logo-img-2x { display: inline-block; }
}
#suggestions {
margin-top: 35px;
color: #ccc;
}
#suggestions a {
color: #666666;
font-weight: 200;
font-size: 14px;
margin: 0 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<p><strong>File not found</strong></p>
<p>
The site configured at this address does not
contain the requested file.
</p>
<p>
If this is your site, make sure that the filename case matches the URL.<br>
For root URLs (like <code>http://example.com/</code>) you must provide an
<code>index.html</code> file.
</p>
<p>
<a href="https://help.github.com/pages/">Read the full documentation</a>
for more information about using <strong>GitHub Pages</strong>.
</p>
<div id="suggestions">
<a href="https://githubstatus.com">GitHub Status</a> &mdash;
<a href="https://twitter.com/githubstatus">@githubstatus</a>
</div>
<a href="/" class="logo logo-img-1x">
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTZCRDY3REIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTZCRDY3RUIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdCQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjdDQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+SM9MCAAAA+5JREFUeNrEV11Ik1EY3s4+ddOp29Q5b0opCgKFsoKoi5Kg6CIhuwi6zLJLoYLopq4qsKKgi4i6CYIoU/q5iDAKs6syoS76IRWtyJ+p7cdt7sf1PGOD+e0c3dygAx/67ZzzPM95/877GYdHRg3ZjMXFxepQKNS6sLCwJxqNNuFpiMfjVs4ZjUa/pmmjeD6VlJS8NpvNT4QQ7mxwjSsJiEQim/1+/9lgMHgIr5ohuxG1WCw9Vqv1clFR0dCqBODElV6v90ogEDjGdYbVjXhpaendioqK07CIR7ZAqE49PT09BPL2PMgTByQGsYiZlQD4uMXtdr+JxWINhgINYhGT2MsKgMrm2dnZXgRXhaHAg5jEJodUAHxux4LudHJE9RdEdA+i3Juz7bGHe4mhE9FNrgwBCLirMFV9Okh5eflFh8PR5nK5nDabrR2BNJlKO0T35+Li4n4+/J+/JQCxhmu5h3uJoXNHPbmWZAHMshWB8l5/ipqammaAf0zPDDx1ONV3vurdidqwAQL+pEc8sLcAe1CCvQ3YHxIW8Pl85xSWNC1hADDIv0rIE/o4J0k3kww4xSlwIhcq3EFFOm7KN/hUGOQkt0CFa5WpNJlMvxBEz/IVQAxg/ZRZl9wiHA63yDYieM7DnLP5CiAGsC7I5sgtYKJGWe2A8seFqgFJrJjEPY1Cn3pJ8/9W1e5VWsFDTEmFrBcoDhZJEQkXuhICMyKpjhahqN21hRYATKfUOlDmkygrR4o4C0VOLGJKrOITKB4jijzdXygBKixyC5TDQdnk/Pz8qRw6oOWGlsTKGOQW6OH6FBWsyePxdOXLTgxiyebILZCjz+GLgMIKnXNzc49YMlcRdHXcSwxFVgTInQhC9G33UhNoJLuqq6t345p9y3eUy8OTk5PjAHuI9uo4b07FBaOhsu0A4Unc+T1TU1Nj3KsSSE5yJ65jqF2DDd8QqWYmAZrIM2VlZTdnZmb6AbpdV9V6ec9znf5Q7HjYumdRE0JOp3MjitO4SFa+cZz8Umqe3TCbSLvdfkR/kWDdNQl5InuTcysOcpFT35ZrbBxx4p3JAHlZVVW1D/634VRt+FvLBgK/v5LV9WS+10xMTEwtRw7XvqOL+e2Q8V3AYIOIAXQ26/heWVnZCVfcyKHg2CBgTpmPmjYM8l24GyaUHyaIh7XwfR9ErE8qHoDfn2LTNAVC0HX6MFcBIP8Bi+6F6cdW/DICkANRfx99fEYFQ7Nph5i/uQiA214gno7K+guhaiKg9gC62+M8eR7XsBsYJ4ilam60Fb7r7uAj8wFyuwM1oIOWgfmDy6RXEEQzJMPe23DXrVS7rtyD3Df8z/FPgAEAzWU5Ku59ZAUAAAAASUVORK5CYII=">
</a>
<a href="/" class="logo logo-img-2x">
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEQUM1QkUxRUI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEQUM1QkUxRkI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdGQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjgwQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+hfPRaQAAB6lJREFUeNrsW2mM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">
</a>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long