Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
574e832152 | ||
|
|
2a84799832 | ||
|
|
8adf3c8261 | ||
|
|
b3245aba9b | ||
|
|
dac2149fbd | ||
|
|
e501f6d125 | ||
|
|
341f26f197 | ||
|
|
5e28643a76 | ||
|
|
b35132451c |
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -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 }}
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
14
plugins/webui/static/css/bootstrap.min.css
vendored
14
plugins/webui/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -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 · 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> —
|
||||
<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
Reference in New Issue
Block a user