diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..740f292
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,24 @@
+[submodule "docs/routers/FredM67-PVRouter-1-phase"]
+ path = docs/routers/FredM67-PVRouter-1-phase
+ url = git@github.com:mathieucarbou/FredM67-PVRouter-1-phase.git
+[submodule "docs/routers/FlaredM67-PVRouter-3-phase"]
+ path = docs/routers/FredM67-PVRouter-3-phase
+ url = git@github.com:mathieucarbou/FredM67-PVRouter-3-phase.git
+[submodule "docs/routers/xlyric-pv-router-esp32"]
+ path = docs/routers/xlyric-pv-router-esp32
+ url = git@github.com:mathieucarbou/xlyric-pv-router-esp32.git
+[submodule "docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn"]
+ path = docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn
+ url = git@github.com:mathieucarbou/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn.git
+[submodule "docs/routers/routeur_le_professolaire"]
+ path = docs/routers/routeur_le_professolaire
+ url = git@github.com:mathieucarbou/routeur_le_professolaire.git
+[submodule "docs/routers/Jetblack31-MaxPV"]
+ path = docs/routers/Jetblack31-MaxPV
+ url = git@github.com:mathieucarbou/Jetblack31-MaxPV.git
+[submodule "docs/routers/FredM67-PVRouter-3-phase"]
+ path = docs/routers/FredM67-PVRouter-3-phase
+ url = git@github.com:mathieucarbou/FredM67-PVRouter-3-phase.git
+[submodule "docs/routers/routeur_solaire"]
+ path = docs/routers/routeur_solaire
+ url = https://github.com/mathieucarbou/SeByDocKy-routeur_solaire
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..cde073f
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,4 @@
+.DS_Store
+/_site
+/.jekyll-cache
+/.sass-cache
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 0000000..e13623d
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+yasolr.carbou.me
\ No newline at end of file
diff --git a/docs/Gemfile b/docs/Gemfile
new file mode 100644
index 0000000..355c167
--- /dev/null
+++ b/docs/Gemfile
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "jekyll"
+
+# If you have any plugins, put them here!
+group :jekyll_plugins do
+ gem "jekyll-remote-theme"
+ gem "jekyll-seo-tag"
+ gem "kramdown-parser-gfm"
+ gem "webrick"
+end
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
new file mode 100644
index 0000000..6690865
--- /dev/null
+++ b/docs/Gemfile.lock
@@ -0,0 +1,85 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.8.6)
+ public_suffix (>= 2.0.2, < 6.0)
+ colorator (1.1.0)
+ concurrent-ruby (1.2.3)
+ em-websocket (0.5.3)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0)
+ eventmachine (1.2.7)
+ ffi (1.16.3)
+ forwardable-extended (2.6.0)
+ google-protobuf (4.26.1-arm64-darwin)
+ rake (>= 13)
+ http_parser.rb (0.8.0)
+ i18n (1.14.5)
+ concurrent-ruby (~> 1.0)
+ jekyll (4.3.3)
+ addressable (~> 2.4)
+ colorator (~> 1.0)
+ em-websocket (~> 0.5)
+ i18n (~> 1.0)
+ jekyll-sass-converter (>= 2.0, < 4.0)
+ jekyll-watch (~> 2.0)
+ kramdown (~> 2.3, >= 2.3.1)
+ kramdown-parser-gfm (~> 1.0)
+ liquid (~> 4.0)
+ mercenary (>= 0.3.6, < 0.5)
+ pathutil (~> 0.9)
+ rouge (>= 3.0, < 5.0)
+ safe_yaml (~> 1.0)
+ terminal-table (>= 1.8, < 4.0)
+ webrick (~> 1.7)
+ jekyll-remote-theme (0.4.3)
+ addressable (~> 2.0)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
+ rubyzip (>= 1.3.0, < 3.0)
+ jekyll-sass-converter (3.0.0)
+ sass-embedded (~> 1.54)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.9.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ mercenary (0.4.0)
+ pathutil (0.16.2)
+ forwardable-extended (~> 2.6)
+ public_suffix (5.0.5)
+ rake (13.2.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ rexml (3.2.6)
+ rouge (4.2.1)
+ rubyzip (2.3.2)
+ safe_yaml (1.0.5)
+ sass-embedded (1.77.1-arm64-darwin)
+ google-protobuf (>= 3.25, < 5.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ unicode-display_width (2.5.0)
+ webrick (1.8.1)
+
+PLATFORMS
+ arm64-darwin-22
+ arm64-darwin-23
+
+DEPENDENCIES
+ jekyll
+ jekyll-remote-theme
+ jekyll-seo-tag
+ kramdown-parser-gfm
+ webrick
+
+BUNDLED WITH
+ 2.4.22
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..d27e6cf
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,4 @@
+Installation: https://jekyllrb.com/docs/installation/macos/
+
+bundle install
+bundle exec jekyll serve --host=0.0.0.0
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..0d1b502
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1,8 @@
+# bundle exec jekyll serve --host=0.0.0.0
+
+title: YaSolR (Yet another Solar Router)
+description: Heat water with your Solar Production Excess!
+remote_theme: pages-themes/cayman@v0.2.0
+plugins:
+ - jekyll-remote-theme
+
\ No newline at end of file
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
new file mode 100644
index 0000000..93ebb64
--- /dev/null
+++ b/docs/_layouts/default.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {% seo %}
+
+
+
+
+
+
+ {% include head-custom.html %}
+
+
+
+ Skip to the content.
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss
new file mode 100644
index 0000000..518ea72
--- /dev/null
+++ b/docs/assets/css/style.scss
@@ -0,0 +1,244 @@
+---
+---
+
+// https://github.com/pages-themes/cayman
+
+// Breakpoints
+$large-breakpoint: 64em !default;
+$medium-breakpoint: 42em !default;
+
+// Headers
+$header-heading-color: #f0f0f0 !default;
+$header-bg-color: #fce23b !default;
+$header-bg-color-secondary: #155799 !default;
+$header-menu-bg-color: #f8c149 !default;
+
+// Text
+$section-headings-color: #159957 !default;
+$body-text-color: #3d3d3e !default;
+$body-link-color: #1e6bb8 !default;
+$blockquote-text-color: #819198 !default;
+
+// Code
+$code-bg-color: #f3f6fa !default;
+$code-text-color: #567482 !default;
+
+// Borders
+$border-color: #dce6f0 !default;
+$table-border-color: #e9ebec !default;
+$hr-border-color: #eff0f1 !default;
+
+@import "{{ site.theme }}";
+
+.page-header {
+ padding: 1rem;
+ font-weight: bolder;
+ color: $header-heading-color;
+
+ .page-description {
+ font-style: italic;
+ font-size: larger;
+ }
+
+ .project-tagline {
+ font-weight: inherit;
+ margin-bottom: 0;
+ font-size: 15px;
+
+ .btn {
+ color: $header-heading-color;
+ background-color: darken($header-menu-bg-color, 20%);
+ border-color: $header-bg-color-secondary;
+ }
+
+ .btn:hover {
+ box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19);
+ }
+
+ .btn-small {
+ padding: 5px 10px 5px 10px;
+ color: $header-heading-color;
+ background-color: darken($header-menu-bg-color, 20%);
+ border-color: $header-bg-color-secondary;
+ }
+
+ .btn-small + .btn-small {
+ //margin-left: 0.5rem;
+ margin-left: 0;
+ }
+
+ .btn-small:hover {
+ box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19);
+ }
+
+ .btn-small.active {
+ background-color: darken($header-bg-color, 20%);
+ }
+ }
+}
+
+#google_translate_element {
+ position: absolute;
+ right: 1rem;
+}
+
+.project-name {
+ margin: 0;
+ font-size: inherit;
+
+ div {
+ margin-left: auto;
+ margin-right: auto;
+ width: fit-content;
+ background-color: #15579980;
+ padding: 10px;
+ border-radius: 15px;
+ }
+}
+
+input.task-list-item-checkbox {
+ margin-right: 7px;
+}
+
+.site-footer-owner {
+ font-weight: normal;
+}
+
+@media screen and (max-width: 730px) {
+ .project-name {
+ div {
+ margin-top: 60px;
+ }
+ }
+}
+
+.main-content {
+ font-size: medium;
+}
+
+:root {
+ --global-tip-block: #42b983;
+ --global-tip-block-bg: #e2f5ec;
+ --global-tip-block-text: #215d42;
+ --global-tip-block-title: #359469;
+
+ --global-note-block: #4262b9;
+ --global-note-block-bg: #e2e5f5;
+ --global-note-block-text: #212a5d;
+ --global-note-block-title: #354b94;
+
+ --global-important-block: #8d42b9;
+ --global-important-block-bg: #efe2f5;
+ --global-important-block-text: #212a5d;
+ --global-important-block-title: #743594;
+
+ --global-warning-block: #e7c000;
+ --global-warning-block-bg: #fff8d8;
+ --global-warning-block-text: #6b5900;
+ --global-warning-block-title: #b29400;
+
+ --global-danger-block: #c00;
+ --global-danger-block-bg: #ffe0e0;
+ --global-danger-block-text: #600;
+ --global-danger-block-title: #c00;
+}
+
+/* Tips, warnings, and dangers */
+blockquote, .main-content blockquote {
+ padding: 0.5rem 1rem;
+
+ &.block-tip {
+ border-color: var(--global-tip-block);
+ background-color: var(--global-tip-block-bg);
+
+ p {
+ color: var(--global-tip-block-text);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--global-tip-block-title);
+ margin-bottom: 0;
+ }
+ }
+
+ &.block-note {
+ border-color: var(--global-note-block);
+ background-color: var(--global-note-block-bg);
+
+ p {
+ color: var(--global-note-block-text);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--global-note-block-title);
+ margin-bottom: 0;
+ }
+ }
+
+ &.block-important {
+ border-color: var(--global-important-block);
+ background-color: var(--global-important-block-bg);
+
+ p {
+ color: var(--global-important-block-text);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--global-important-block-title);
+ margin-bottom: 0;
+ }
+ }
+
+ &.block-warning {
+ border-color: var(--global-warning-block);
+ background-color: var(--global-warning-block-bg);
+
+ p {
+ color: var(--global-warning-block-text);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--global-warning-block-title);
+ margin-bottom: 0;
+ }
+ }
+
+ &.block-danger {
+ border-color: var(--global-danger-block);
+ background-color: var(--global-danger-block-bg);
+
+ p {
+ color: var(--global-danger-block-text);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--global-danger-block-title);
+ margin-bottom: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/img/Github_Donate.png b/docs/assets/img/Github_Donate.png
new file mode 100644
index 0000000..96b6592
Binary files /dev/null and b/docs/assets/img/Github_Donate.png differ
diff --git a/docs/assets/img/Paypal_Donate.png b/docs/assets/img/Paypal_Donate.png
new file mode 100644
index 0000000..a0f4428
Binary files /dev/null and b/docs/assets/img/Paypal_Donate.png differ
diff --git a/docs/assets/img/builds/rnd_box_1.jpeg b/docs/assets/img/builds/rnd_box_1.jpeg
new file mode 100644
index 0000000..243d277
Binary files /dev/null and b/docs/assets/img/builds/rnd_box_1.jpeg differ
diff --git a/docs/assets/img/builds/rnd_box_2.jpeg b/docs/assets/img/builds/rnd_box_2.jpeg
new file mode 100644
index 0000000..1f0796a
Binary files /dev/null and b/docs/assets/img/builds/rnd_box_2.jpeg differ
diff --git a/docs/assets/img/hardware/BTA40-800B.jpeg b/docs/assets/img/hardware/BTA40-800B.jpeg
new file mode 100644
index 0000000..6d6e75a
Binary files /dev/null and b/docs/assets/img/hardware/BTA40-800B.jpeg differ
diff --git a/docs/assets/img/hardware/DIN_1_Relay.jpeg b/docs/assets/img/hardware/DIN_1_Relay.jpeg
new file mode 100644
index 0000000..864a782
Binary files /dev/null and b/docs/assets/img/hardware/DIN_1_Relay.jpeg differ
diff --git a/docs/assets/img/hardware/DIN_2_Relay.jpeg b/docs/assets/img/hardware/DIN_2_Relay.jpeg
new file mode 100644
index 0000000..00a59da
Binary files /dev/null and b/docs/assets/img/hardware/DIN_2_Relay.jpeg differ
diff --git a/docs/assets/img/hardware/DIN_HDR-15-5.jpeg b/docs/assets/img/hardware/DIN_HDR-15-5.jpeg
new file mode 100644
index 0000000..59d900d
Binary files /dev/null and b/docs/assets/img/hardware/DIN_HDR-15-5.jpeg differ
diff --git a/docs/assets/img/hardware/DIN_SSR_Clip.png b/docs/assets/img/hardware/DIN_SSR_Clip.png
new file mode 100644
index 0000000..1dbdb34
Binary files /dev/null and b/docs/assets/img/hardware/DIN_SSR_Clip.png differ
diff --git a/docs/assets/img/hardware/DS18B20.jpeg b/docs/assets/img/hardware/DS18B20.jpeg
new file mode 100644
index 0000000..e558a07
Binary files /dev/null and b/docs/assets/img/hardware/DS18B20.jpeg differ
diff --git a/docs/assets/img/hardware/Distrib_DIN.jpeg b/docs/assets/img/hardware/Distrib_DIN.jpeg
new file mode 100644
index 0000000..92a99f4
Binary files /dev/null and b/docs/assets/img/hardware/Distrib_DIN.jpeg differ
diff --git a/docs/assets/img/hardware/DupontWire.jpeg b/docs/assets/img/hardware/DupontWire.jpeg
new file mode 100644
index 0000000..de3f674
Binary files /dev/null and b/docs/assets/img/hardware/DupontWire.jpeg differ
diff --git a/docs/assets/img/hardware/ESP32-S3.jpeg b/docs/assets/img/hardware/ESP32-S3.jpeg
new file mode 100644
index 0000000..8f09a27
Binary files /dev/null and b/docs/assets/img/hardware/ESP32-S3.jpeg differ
diff --git a/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg b/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg
new file mode 100644
index 0000000..a102810
Binary files /dev/null and b/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg differ
diff --git a/docs/assets/img/hardware/ESP32_NodeMCU.jpeg b/docs/assets/img/hardware/ESP32_NodeMCU.jpeg
new file mode 100644
index 0000000..21401c3
Binary files /dev/null and b/docs/assets/img/hardware/ESP32_NodeMCU.jpeg differ
diff --git a/docs/assets/img/hardware/Electric_Box.jpeg b/docs/assets/img/hardware/Electric_Box.jpeg
new file mode 100644
index 0000000..a6f052a
Binary files /dev/null and b/docs/assets/img/hardware/Electric_Box.jpeg differ
diff --git a/docs/assets/img/hardware/Extension_Board.jpeg b/docs/assets/img/hardware/Extension_Board.jpeg
new file mode 100644
index 0000000..b0feec3
Binary files /dev/null and b/docs/assets/img/hardware/Extension_Board.jpeg differ
diff --git a/docs/assets/img/hardware/Heat_Sink.jpeg b/docs/assets/img/hardware/Heat_Sink.jpeg
new file mode 100644
index 0000000..cc4008c
Binary files /dev/null and b/docs/assets/img/hardware/Heat_Sink.jpeg differ
diff --git a/docs/assets/img/hardware/JSY-MK-194T_1.jpeg b/docs/assets/img/hardware/JSY-MK-194T_1.jpeg
new file mode 100644
index 0000000..85486a5
Binary files /dev/null and b/docs/assets/img/hardware/JSY-MK-194T_1.jpeg differ
diff --git a/docs/assets/img/hardware/JSY-MK-194T_2.jpeg b/docs/assets/img/hardware/JSY-MK-194T_2.jpeg
new file mode 100644
index 0000000..30bc3de
Binary files /dev/null and b/docs/assets/img/hardware/JSY-MK-194T_2.jpeg differ
diff --git a/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg b/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg
new file mode 100644
index 0000000..7178070
Binary files /dev/null and b/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg differ
diff --git a/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg b/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg
new file mode 100644
index 0000000..e322c5e
Binary files /dev/null and b/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg differ
diff --git a/docs/assets/img/hardware/LEDs.jpeg b/docs/assets/img/hardware/LEDs.jpeg
new file mode 100644
index 0000000..756dd53
Binary files /dev/null and b/docs/assets/img/hardware/LEDs.jpeg differ
diff --git a/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg b/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg
new file mode 100644
index 0000000..a602156
Binary files /dev/null and b/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg differ
diff --git a/docs/assets/img/hardware/LSA-H3P50YB.jpeg b/docs/assets/img/hardware/LSA-H3P50YB.jpeg
new file mode 100644
index 0000000..6ef0949
Binary files /dev/null and b/docs/assets/img/hardware/LSA-H3P50YB.jpeg differ
diff --git a/docs/assets/img/hardware/PWM_33_0-10.jpeg b/docs/assets/img/hardware/PWM_33_0-10.jpeg
new file mode 100644
index 0000000..c0ed7b3
Binary files /dev/null and b/docs/assets/img/hardware/PWM_33_0-10.jpeg differ
diff --git a/docs/assets/img/hardware/PZEM-004T.jpeg b/docs/assets/img/hardware/PZEM-004T.jpeg
new file mode 100644
index 0000000..357807a
Binary files /dev/null and b/docs/assets/img/hardware/PZEM-004T.jpeg differ
diff --git a/docs/assets/img/hardware/Passive_Buzzer.jpeg b/docs/assets/img/hardware/Passive_Buzzer.jpeg
new file mode 100644
index 0000000..d653f17
Binary files /dev/null and b/docs/assets/img/hardware/Passive_Buzzer.jpeg differ
diff --git a/docs/assets/img/hardware/Pigtail_Antenna.jpeg b/docs/assets/img/hardware/Pigtail_Antenna.jpeg
new file mode 100644
index 0000000..7e42d41
Binary files /dev/null and b/docs/assets/img/hardware/Pigtail_Antenna.jpeg differ
diff --git a/docs/assets/img/hardware/PushButton.jpeg b/docs/assets/img/hardware/PushButton.jpeg
new file mode 100644
index 0000000..71728c1
Binary files /dev/null and b/docs/assets/img/hardware/PushButton.jpeg differ
diff --git a/docs/assets/img/hardware/RC_Snubber.jpeg b/docs/assets/img/hardware/RC_Snubber.jpeg
new file mode 100644
index 0000000..fabf7a2
Binary files /dev/null and b/docs/assets/img/hardware/RC_Snubber.jpeg differ
diff --git a/docs/assets/img/hardware/Random_SSR.jpeg b/docs/assets/img/hardware/Random_SSR.jpeg
new file mode 100644
index 0000000..a7324e6
Binary files /dev/null and b/docs/assets/img/hardware/Random_SSR.jpeg differ
diff --git a/docs/assets/img/hardware/Random_SSR_EARU.jpeg b/docs/assets/img/hardware/Random_SSR_EARU.jpeg
new file mode 100644
index 0000000..687b784
Binary files /dev/null and b/docs/assets/img/hardware/Random_SSR_EARU.jpeg differ
diff --git a/docs/assets/img/hardware/Raspberry_Fans.jpeg b/docs/assets/img/hardware/Raspberry_Fans.jpeg
new file mode 100644
index 0000000..b6de5f6
Binary files /dev/null and b/docs/assets/img/hardware/Raspberry_Fans.jpeg differ
diff --git a/docs/assets/img/hardware/Robodyn_24A.jpeg b/docs/assets/img/hardware/Robodyn_24A.jpeg
new file mode 100644
index 0000000..ded31f3
Binary files /dev/null and b/docs/assets/img/hardware/Robodyn_24A.jpeg differ
diff --git a/docs/assets/img/hardware/Robodyn_40A.jpeg b/docs/assets/img/hardware/Robodyn_40A.jpeg
new file mode 100644
index 0000000..d7e29bf
Binary files /dev/null and b/docs/assets/img/hardware/Robodyn_40A.jpeg differ
diff --git a/docs/assets/img/hardware/SH1106.jpeg b/docs/assets/img/hardware/SH1106.jpeg
new file mode 100644
index 0000000..ce6ebb0
Binary files /dev/null and b/docs/assets/img/hardware/SH1106.jpeg differ
diff --git a/docs/assets/img/hardware/SH1107.jpeg b/docs/assets/img/hardware/SH1107.jpeg
new file mode 100644
index 0000000..325b1e7
Binary files /dev/null and b/docs/assets/img/hardware/SH1107.jpeg differ
diff --git a/docs/assets/img/hardware/SSD1306.jpeg b/docs/assets/img/hardware/SSD1306.jpeg
new file mode 100644
index 0000000..2074d84
Binary files /dev/null and b/docs/assets/img/hardware/SSD1306.jpeg differ
diff --git a/docs/assets/img/hardware/SSR_40A_DA.jpeg b/docs/assets/img/hardware/SSR_40A_DA.jpeg
new file mode 100644
index 0000000..c7279bb
Binary files /dev/null and b/docs/assets/img/hardware/SSR_40A_DA.jpeg differ
diff --git a/docs/assets/img/hardware/SSR_Heat_Sink.png b/docs/assets/img/hardware/SSR_Heat_Sink.png
new file mode 100644
index 0000000..26dc440
Binary files /dev/null and b/docs/assets/img/hardware/SSR_Heat_Sink.png differ
diff --git a/docs/assets/img/hardware/Shelly_Addon.jpeg b/docs/assets/img/hardware/Shelly_Addon.jpeg
new file mode 100644
index 0000000..b187dd2
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Addon.jpeg differ
diff --git a/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg b/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg
new file mode 100644
index 0000000..e3a1994
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg differ
diff --git a/docs/assets/img/hardware/Shelly_DS18.jpeg b/docs/assets/img/hardware/Shelly_DS18.jpeg
new file mode 100644
index 0000000..0cc3990
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_DS18.jpeg differ
diff --git a/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg b/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg
new file mode 100644
index 0000000..bee6ac2
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg differ
diff --git a/docs/assets/img/hardware/Shelly_EM.png b/docs/assets/img/hardware/Shelly_EM.png
new file mode 100644
index 0000000..b8b5cfd
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_EM.png differ
diff --git a/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg b/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg
new file mode 100644
index 0000000..199dd08
Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg differ
diff --git a/docs/assets/img/hardware/WT32-ETH01.jpeg b/docs/assets/img/hardware/WT32-ETH01.jpeg
new file mode 100644
index 0000000..0ec092e
Binary files /dev/null and b/docs/assets/img/hardware/WT32-ETH01.jpeg differ
diff --git a/docs/assets/img/hardware/ZCD.jpeg b/docs/assets/img/hardware/ZCD.jpeg
new file mode 100644
index 0000000..9a8e09b
Binary files /dev/null and b/docs/assets/img/hardware/ZCD.jpeg differ
diff --git a/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg b/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg
new file mode 100644
index 0000000..61ad835
Binary files /dev/null and b/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg differ
diff --git a/docs/assets/img/hardware/jsy-enclosure.jpeg b/docs/assets/img/hardware/jsy-enclosure.jpeg
new file mode 100644
index 0000000..b731e54
Binary files /dev/null and b/docs/assets/img/hardware/jsy-enclosure.jpeg differ
diff --git a/docs/assets/img/hardware/lsa_heat_sink.jpeg b/docs/assets/img/hardware/lsa_heat_sink.jpeg
new file mode 100644
index 0000000..56ae722
Binary files /dev/null and b/docs/assets/img/hardware/lsa_heat_sink.jpeg differ
diff --git a/docs/assets/img/hardware/shelly_dimmer_with_addon.jpeg b/docs/assets/img/hardware/shelly_dimmer_with_addon.jpeg
new file mode 100644
index 0000000..4f92f96
Binary files /dev/null and b/docs/assets/img/hardware/shelly_dimmer_with_addon.jpeg differ
diff --git a/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg b/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg
new file mode 100644
index 0000000..4cb83f2
Binary files /dev/null and b/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg differ
diff --git a/docs/assets/img/hardware/shelly_solar_diverter_poc2.jpeg b/docs/assets/img/hardware/shelly_solar_diverter_poc2.jpeg
new file mode 100644
index 0000000..4c08fd0
Binary files /dev/null and b/docs/assets/img/hardware/shelly_solar_diverter_poc2.jpeg differ
diff --git a/docs/assets/img/logo-60px.png b/docs/assets/img/logo-60px.png
new file mode 100644
index 0000000..dd9974d
Binary files /dev/null and b/docs/assets/img/logo-60px.png differ
diff --git "a/docs/assets/img/logo-640\303\227320px.png" "b/docs/assets/img/logo-640\303\227320px.png"
new file mode 100644
index 0000000..e5c5025
Binary files /dev/null and "b/docs/assets/img/logo-640\303\227320px.png" differ
diff --git a/docs/assets/img/logo-big.png b/docs/assets/img/logo-big.png
new file mode 100644
index 0000000..3e4a2b5
Binary files /dev/null and b/docs/assets/img/logo-big.png differ
diff --git a/docs/assets/img/logo.png b/docs/assets/img/logo.png
new file mode 100644
index 0000000..427bed0
Binary files /dev/null and b/docs/assets/img/logo.png differ
diff --git a/docs/assets/img/measurements/20240403162331.set b/docs/assets/img/measurements/20240403162331.set
new file mode 100644
index 0000000..be3ae8e
--- /dev/null
+++ b/docs/assets/img/measurements/20240403162331.set
@@ -0,0 +1,326 @@
+#Wed Apr 03 16:23:34 CEST 2024
+SingleTrg.LIN.dlc=0
+Zoom.middleofborders=0
+CH2.Edge.raisefall=0
+CH1.Video.syncValue=1
+Bus1.CAN.source=0
+CH2.probeMultiIdx=1
+SingleTrg.CAN.baudCustom=10000.0
+NetWork.setSubnetmask=255.255.255.0
+CH1.on=1
+CH2.Slope.condition=0
+CH2.Slope.sweep=0
+CH2.amperePerVolt=10000
+CH1.measureCurrentOn=0
+triggerChannelIndex=0
+CH1.Pulse.coupling=0
+Zoom.onoff=0
+Bus1.LIN.signal=0
+CH2.Slope.uppest=1
+CH1.probeMultiIdx=2
+Bus1.protocol=0
+CH4.Edge.holdoff=1
+CH1.Pulse.sweep=0
+Bus2.display=0
+SingleTrg.holdoff_v=1
+CH1.amperePerVolt=10000
+SingleTrg.Edge.coupling=1
+NetWork.setMac=20.127.15.207.245.4
+SingleTrg.Slope.sweep=0
+CH1.forcebandlimit=0
+MarkCursor.y2=360
+ComputeFreqTimes=5
+CH3.couplingIdx=1
+MarkCursor.y1=142
+CH1.pos0=0
+SingleTrg.Video.syncValue=0
+SingleTrg.CAN.level=0
+Bus2.LIN.baudrate=2400.0
+SingleTrg.CAN.type=0
+CH4.Pulse.argument=3
+Sample.mode=0
+Country=
+Math.ffton=0
+SingleTrg.CAN.baud=0
+SingleTrg.trgMode=0
+CH4.Edge.coupling=0
+SingleTrg.Pulse.holdoff=1
+CH1.Slope.condition=0
+RecordIntervalTime=40
+CH4.Pulse.voltsense=0
+MarkCursor.x2=662
+MarkCursor.x1=362
+MeasureDelayCode=
+Sine.amplitude=1.0
+CH3.Edge.voltsense=0
+CH4.inverse=0
+Pulse.dutyCycle=50.0
+lowMachineChannels=2
+selectedwfIdx=0
+Rule.current=1,0.2,0.2
+Bus2.CAN.brtype=4
+SingleTrg.LIN.voltsense=0
+CH2.bandlimit=0
+SingleTrg.Slope.lowest=75
+CH2.on=1
+CH4.Slope.uppest=1
+CH3.Edge.coupling=0
+Bus2.CAN.samplePoint=50.0
+CH4.Pulse.condition=0
+ExportPath=/Users/mat/Downloads
+Rule.msg=0
+Bus2.LIN.source=0
+FFTCursor.onFrebaseMark=0
+CH4.vbIdx=6
+CH4.Video.sync=0
+Pulse.offset=0.0
+CH1.Pulse.argument=3
+bus.thresholds.4=0
+bus.thresholds.3=0
+bus.thresholds.2=0
+bus.thresholds.1=0
+FGen.selection=0
+CH3.Pulse.coupling=0
+Zoom.tbz=16
+CH4.trgMode=0
+CH1.Edge.holdoff=1
+CH3.Pulse.holdoff=1
+CH2.Edge.coupling=0
+Bus1.LIN.brtype=2
+SingleTrg.Slope.condition=1
+CH1.Slope.argument=3
+Ramp.symmetry=50.0
+SaveimgPath=/Users/mat/Downloads
+NetWork.linkIP=192.168.1.172
+CH3.Pulse.voltsense=0
+Tune.tid=0
+Bus1.CAN.signal=0
+CH1.Edge.coupling=0
+CH2.Pulse.holdoff=1
+CH3.Slope.sweep=0
+CH4.measureCurrentOn=1
+CH4.Slope.holdoff=1
+CH3.vbIdx=10
+SingleTrg.CAN.id=0
+Bus1.CAN.samplePoint=50.0
+Ramp.amplitude=1.0
+SingleTrg.Video.module=0
+SingleTrg.Video.sync=0
+CH2.inverse=0
+NetWork.setGateway=192.168.8.1
+SingleTrg.CAN.when=0
+CH2.Pulse.sweep=0
+Math.operation=0
+CH1.Pulse.holdoff=1
+Ramp.frequency=1000.0
+CH3.Edge.sweep=0
+CH3.on=1
+Bus1.LIN.samplePoint=50.0
+Timebase.index=18
+CH3.Slope.holdoff=1
+CH1.Slope.uppest=1
+CH4.Video.syncValue=1
+RecordPath=wave1.cap
+SingleTrg.Edge.voltsense=0
+DeepMemory=4
+CH4.couplingIdx=0
+LineLink=1
+CH3.Video.module=0
+CH2.Video.sync=0
+CH2.Pulse.voltsense=0
+Math.m2=1
+NetWork.setPort=8866
+STYLE_TYPE=0
+Math.m1=0
+CH2.trgMode=0
+CustomizePages=1,6,3,8,11
+CH4.Video.holdoff=1
+Language=en
+SingleTrg.Pulse.sweep=0
+Bus1.LIN.baudrate=2400.0
+MarkCursor.CHNum=0
+SingleTrg.Pulse.coupling=1
+Bus2.CAN.source=0
+circleSerialPort=2
+ReferenceObjsUseable=
+SingleTrg.holdoff_en=0
+SingleTrg.LIN.baud=0
+SingleTrg.selectIndex=0
+Bus2.CAN.baudrate=10000.0
+Print.rightEdgeLength=20
+Rule.outputRule=0
+FFTCursor.onVoltbaseMark=0
+ReferenceIsValid=1,0,0,0,
+Print.isVertical=1
+CH4.Slope.argument=3
+Bus2.LIN.signal=0
+CH3.Video.syncValue=1
+SingleTrg.LIN.id=0
+CH4.Slope.condition=0
+CH2.Pulse.coupling=0
+NetWork.Sync_out=1
+SingleTrg.LIN.coupling=0
+PlayPath=wave1.cap
+Rule.ring=0
+SingleTrg.CAN.data=0.0.0.0.0.0.0.0
+CH4.Edge.raisefall=0
+debug=1
+Square.frequency=1000.0
+CH4.on=1
+Sine.offset=0.0
+CH1.bandlimit=0
+Bus2.CAN=CAN
+SingleTrg.LIN.stype=0
+HorTrgPos=0
+ScpiPort=5188
+Rule.stopOnOutput=0
+Pulse.amplitude=1.0
+CH2.Slope.lowest=1
+CH1.Edge.sweep=0
+CH4.bandlimit=0
+CH4.Pulse.sweep=0
+CH4.Slope.sweep=0
+Pulse.frequency=1000.0
+Ramp.offset=0.0
+SingleTrg.CAN.samplePoint=50
+SingleTrg.Slope.holdoff=1
+SingleTrg.LIN.baudCustom=10000.0
+XYModeOn=0
+triggerChannelMode=0
+CH1.Edge.raisefall=0
+Bus1.CAN=CAN
+Math.fftscale=0
+MeasureTypes=17,4,13,12,11,
+CH3.Pulse.sweep=0
+Rule.rules=1,0.2,0.2 ; 2,0.5,0.5 ;
+CH2.Video.syncValue=1
+SingleTrg.LIN.samplePoint=50
+CH1.couplingIdx=0
+CH3.Slope.condition=0
+SingleTrg.Pulse.voltsense=0
+CH4.probeMultiIdx=0
+CH3.Pulse.condition=0
+SingleTrg.CAN.voltsense=0
+Bus2.protocol=0
+CH4.amperePerVolt=10000
+TipsWindowShow=0
+CH3.measureCurrentOn=0
+CH4.Slope.lowest=1
+Bus2.LIN.brtype=2
+SingleTrg.LIN.when=0
+RecordEndFrame=300
+Bus1.display=0
+SingleTrg.Video.holdoff=1
+CH3.Edge.holdoff=1
+CH4.rgb=CC00FF
+WaveFormY=1
+WaveFormX=0
+CH2.Video.module=0
+CH1.trgMode=0
+CH1.Video.sync=0
+ReferenceObjsNames=0,0,0,0,0,0,0,0,
+Print.isPaintWFBG=0
+CH3.amperePerVolt=10000
+CH4.pos0=0
+Math.fftwnd=0
+CH3.Pulse.argument=3
+maxFailureTime=20
+CH2.measureCurrentOn=0
+Bus2.CAN.signal=0
+Zoom.ztimebaseidx=8
+CH2.Slope.holdoff=1
+OpenfilePath=
+CH2.vbIdx=7
+Math.fftvaluetype=1
+SingleTrg.Slope.uppest=75
+CH2.Edge.voltsense=0
+CH3.Slope.argument=3
+CH3.rgb=66CCFF
+CH2.Pulse.condition=0
+Bus1.CAN.baudrate=10000.0
+CH3.inverse=0
+CH4.Edge.sweep=0
+CH1.Slope.holdoff=1
+CH3.Edge.raisefall=0
+NetWork.setIP=192.168.8.172
+CH3.Video.holdoff=1
+dbgbtns=0
+PersistenceIndex=0
+Bus1.CAN.brtype=4
+RecordCounter=0
+Square.amplitude=1.0
+CH3.Slope.uppest=1
+CH1.Slope.sweep=0
+Math.mathon=0
+CH4.Video.module=0
+Bus1.LIN.source=0
+CH3.Video.sync=0
+CH2.rgb=FFFF00
+MeasureTimes=20
+Square.offset=0.0
+CH2.Video.holdoff=1
+SingleTrg.LIN.level=0
+Print.leftEdgeLength=20
+CH1.Pulse.voltsense=0
+CH1.vbIdx=9
+SingleTrg.LIN.data=0.0.0.0.0.0.0.0
+CH1.Slope.lowest=1
+CH3.trgMode=0
+productParam=VDS6104
+SingleTrg.CAN.coupling=0
+SingleTrg.CAN.stype=0
+CH3.pos0=0
+Sample.precision=0
+CH2.forcebandlimit=0
+CH2.couplingIdx=1
+CH1.Video.holdoff=1
+CH3.bandlimit=0
+CH1.Pulse.condition=0
+Print.downEdgeLength=20
+CH1.rgb=FF0000
+SingleTrg.CAN.dlc=0
+SingleTrg.Edge.sweep=0
+GridBrightness=100
+SingleTrg.Pulse.argument=3
+Sine.frequency=1000.0
+CH4.Pulse.holdoff=1
+NetWork.linkPort=2000
+MarkCursor.onTimebase=0
+MarkCursor.onVoltbase=0
+CH4.Edge.voltsense=0
+CH1.inverse=0
+Tune.chidx=0
+Bus2.LIN=LIN
+SingleTrg.Edge.holdoff=1
+SingleTrg.sweep=0
+Rule.ruleOnPF=1
+MeasureChannels=0,1,2,3,
+Variant=
+SingleTrg.Slope.argument=3
+FFTCursor.y2=0
+FFTCursor.y1=0
+Bus2.LIN.samplePoint=50.0
+CH2.Edge.sweep=0
+CH3.Slope.lowest=1
+Zoom.offset=0
+SingleTrg.Edge.raisefall=0
+CH2.Pulse.argument=3
+CH2.Edge.holdoff=1
+productVersion=V2.0.2.011
+SingleTrg.Pulse.condition=1
+CH1.Video.module=0
+FFTCursor.x2=-6
+FFTCursor.x1=-6
+CH3.probeMultiIdx=0
+CH2.pos0=0
+Print.upEdgeLength=20
+CH4.Pulse.coupling=0
+\u00EF\u00BB\u00BF\#\u00E4\u00BB\u00A5\u00E4\u00B8\u008B\u00E4\u00B8\u00BA\u00E4\u00BA\u00A7\u00E5\u0093\u0081\u00E5\u008F\u00AF\u00E5\u008F\u0098\u00E5\u00AD\u0098\u00E5\u0082\u00A8\u00E9\u0085\u008D\u00E7\u00BD\u00AE\u00E4\u00BF\u00A1\u00E6\u0081\u00AF\u00E7\u009A\u0084\u00E9\u00BB\u0098\u00E8\u00AE\u00A4\u00E5\u0080\u00BC=
+Math.fftchl=0
+Bus1.LIN=LIN
+log_type=4
+getDataPeroid=40
+CH1.Edge.voltsense=0
+CH2.Slope.argument=3
+ReferenceSourceIdx=0
+Sample.avgTimes=1
diff --git a/docs/assets/img/measurements/Burst_20.png b/docs/assets/img/measurements/Burst_20.png
new file mode 100644
index 0000000..1d5c605
Binary files /dev/null and b/docs/assets/img/measurements/Burst_20.png differ
diff --git a/docs/assets/img/measurements/Burst_50.png b/docs/assets/img/measurements/Burst_50.png
new file mode 100644
index 0000000..994c956
Binary files /dev/null and b/docs/assets/img/measurements/Burst_50.png differ
diff --git a/docs/assets/img/measurements/CEI 61000-3-2.png b/docs/assets/img/measurements/CEI 61000-3-2.png
new file mode 100644
index 0000000..35cb888
Binary files /dev/null and b/docs/assets/img/measurements/CEI 61000-3-2.png differ
diff --git a/docs/assets/img/measurements/Clyric_20.png b/docs/assets/img/measurements/Clyric_20.png
new file mode 100644
index 0000000..a61e0f4
Binary files /dev/null and b/docs/assets/img/measurements/Clyric_20.png differ
diff --git a/docs/assets/img/measurements/Clyric_50.png b/docs/assets/img/measurements/Clyric_50.png
new file mode 100644
index 0000000..6477a27
Binary files /dev/null and b/docs/assets/img/measurements/Clyric_50.png differ
diff --git a/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg b/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg
new file mode 100644
index 0000000..a0d6e71
Binary files /dev/null and b/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg differ
diff --git a/docs/assets/img/measurements/Dimmer 99_.jpeg b/docs/assets/img/measurements/Dimmer 99_.jpeg
new file mode 100644
index 0000000..de50192
Binary files /dev/null and b/docs/assets/img/measurements/Dimmer 99_.jpeg differ
diff --git a/docs/assets/img/measurements/H15.png b/docs/assets/img/measurements/H15.png
new file mode 100644
index 0000000..4a9e9ab
Binary files /dev/null and b/docs/assets/img/measurements/H15.png differ
diff --git a/docs/assets/img/measurements/H3.png b/docs/assets/img/measurements/H3.png
new file mode 100644
index 0000000..756994e
Binary files /dev/null and b/docs/assets/img/measurements/H3.png differ
diff --git a/docs/assets/img/measurements/Harmoniques.jpeg b/docs/assets/img/measurements/Harmoniques.jpeg
new file mode 100644
index 0000000..1d2c20a
Binary files /dev/null and b/docs/assets/img/measurements/Harmoniques.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_600W_Out.jpeg b/docs/assets/img/measurements/Oscillo_600W_Out.jpeg
new file mode 100644
index 0000000..7508dbd
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_600W_Out.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Bypass.jpeg b/docs/assets/img/measurements/Oscillo_Bypass.jpeg
new file mode 100644
index 0000000..3060178
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Bypass.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg b/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg
new file mode 100644
index 0000000..16b5009
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg b/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg
new file mode 100644
index 0000000..0a15053
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg
new file mode 100644
index 0000000..1ac9efe
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg
new file mode 100644
index 0000000..d40f999
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg
new file mode 100644
index 0000000..f3074d9
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_ZCD.jpeg b/docs/assets/img/measurements/Oscillo_ZCD.jpeg
new file mode 100644
index 0000000..e316464
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_ZCD.jpeg differ
diff --git a/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg b/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg
new file mode 100644
index 0000000..e140a4a
Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg differ
diff --git a/docs/assets/img/measurements/Robodyn_duty_10.png b/docs/assets/img/measurements/Robodyn_duty_10.png
new file mode 100644
index 0000000..a91448c
Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_10.png differ
diff --git a/docs/assets/img/measurements/Robodyn_duty_2047.png b/docs/assets/img/measurements/Robodyn_duty_2047.png
new file mode 100644
index 0000000..3e4125a
Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_2047.png differ
diff --git a/docs/assets/img/measurements/Robodyn_duty_4090.png b/docs/assets/img/measurements/Robodyn_duty_4090.png
new file mode 100644
index 0000000..c7cf8cf
Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_4090.png differ
diff --git a/docs/assets/img/measurements/VDS6104_10.png b/docs/assets/img/measurements/VDS6104_10.png
new file mode 100644
index 0000000..b8d946b
Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_10.png differ
diff --git a/docs/assets/img/measurements/VDS6104_50.png b/docs/assets/img/measurements/VDS6104_50.png
new file mode 100644
index 0000000..ff7e061
Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_50.png differ
diff --git a/docs/assets/img/measurements/VDS6104_90.png b/docs/assets/img/measurements/VDS6104_90.png
new file mode 100644
index 0000000..e13efff
Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_90.png differ
diff --git a/docs/assets/img/measurements/ZCD DanStar Falling.jpeg b/docs/assets/img/measurements/ZCD DanStar Falling.jpeg
new file mode 100644
index 0000000..7d7ce76
Binary files /dev/null and b/docs/assets/img/measurements/ZCD DanStar Falling.jpeg differ
diff --git a/docs/assets/img/measurements/ZCD DanStar Rising.jpeg b/docs/assets/img/measurements/ZCD DanStar Rising.jpeg
new file mode 100644
index 0000000..67ac63b
Binary files /dev/null and b/docs/assets/img/measurements/ZCD DanStar Rising.jpeg differ
diff --git a/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg b/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg
new file mode 100644
index 0000000..ad14b7f
Binary files /dev/null and b/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg differ
diff --git a/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg b/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg
new file mode 100644
index 0000000..88a500c
Binary files /dev/null and b/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg differ
diff --git a/docs/assets/img/measurements/ZCD_duty_10.png b/docs/assets/img/measurements/ZCD_duty_10.png
new file mode 100644
index 0000000..ab91414
Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_10.png differ
diff --git a/docs/assets/img/measurements/ZCD_duty_2047.png b/docs/assets/img/measurements/ZCD_duty_2047.png
new file mode 100644
index 0000000..47884e0
Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_2047.png differ
diff --git a/docs/assets/img/measurements/ZCD_duty_4090.png b/docs/assets/img/measurements/ZCD_duty_4090.png
new file mode 100644
index 0000000..2d1418d
Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_4090.png differ
diff --git a/docs/assets/img/schemas/Breaker_16A.jpeg b/docs/assets/img/schemas/Breaker_16A.jpeg
new file mode 100644
index 0000000..d1f2ddf
Binary files /dev/null and b/docs/assets/img/schemas/Breaker_16A.jpeg differ
diff --git a/docs/assets/img/schemas/Breaker_2A.jpeg b/docs/assets/img/schemas/Breaker_2A.jpeg
new file mode 100644
index 0000000..cefb6ea
Binary files /dev/null and b/docs/assets/img/schemas/Breaker_2A.jpeg differ
diff --git a/docs/assets/img/schemas/Contact_25A.jpeg b/docs/assets/img/schemas/Contact_25A.jpeg
new file mode 100644
index 0000000..0776e1f
Binary files /dev/null and b/docs/assets/img/schemas/Contact_25A.jpeg differ
diff --git a/docs/assets/img/schemas/LSA.png b/docs/assets/img/schemas/LSA.png
new file mode 100644
index 0000000..d59df3f
Binary files /dev/null and b/docs/assets/img/schemas/LSA.png differ
diff --git a/docs/assets/img/schemas/RC_Snubber.jpeg b/docs/assets/img/schemas/RC_Snubber.jpeg
new file mode 100644
index 0000000..7d2a373
Binary files /dev/null and b/docs/assets/img/schemas/RC_Snubber.jpeg differ
diff --git a/docs/assets/img/schemas/Relay.png b/docs/assets/img/schemas/Relay.png
new file mode 100644
index 0000000..e3d8b13
Binary files /dev/null and b/docs/assets/img/schemas/Relay.png differ
diff --git a/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png b/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png
new file mode 100644
index 0000000..2c827f1
Binary files /dev/null and b/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png differ
diff --git a/docs/assets/img/schemas/Shelly_EM_50.png b/docs/assets/img/schemas/Shelly_EM_50.png
new file mode 100644
index 0000000..24bcf7a
Binary files /dev/null and b/docs/assets/img/schemas/Shelly_EM_50.png differ
diff --git a/docs/assets/img/schemas/Solar_Router_Diverter.jpg b/docs/assets/img/schemas/Solar_Router_Diverter.jpg
new file mode 100644
index 0000000..b4f7b20
Binary files /dev/null and b/docs/assets/img/schemas/Solar_Router_Diverter.jpg differ
diff --git a/docs/assets/img/schemas/water_tank.jpeg b/docs/assets/img/schemas/water_tank.jpeg
new file mode 100644
index 0000000..fd0a8b6
Binary files /dev/null and b/docs/assets/img/schemas/water_tank.jpeg differ
diff --git a/docs/assets/img/screenshots/Captive_Portal.jpeg b/docs/assets/img/screenshots/Captive_Portal.jpeg
new file mode 100644
index 0000000..4d2fcff
Binary files /dev/null and b/docs/assets/img/screenshots/Captive_Portal.jpeg differ
diff --git a/docs/assets/img/screenshots/Espressif_Flash_Tool.png b/docs/assets/img/screenshots/Espressif_Flash_Tool.png
new file mode 100644
index 0000000..21697e0
Binary files /dev/null and b/docs/assets/img/screenshots/Espressif_Flash_Tool.png differ
diff --git a/docs/assets/img/screenshots/config.jpeg b/docs/assets/img/screenshots/config.jpeg
new file mode 100644
index 0000000..68a3b27
Binary files /dev/null and b/docs/assets/img/screenshots/config.jpeg differ
diff --git a/docs/assets/img/screenshots/console.jpeg b/docs/assets/img/screenshots/console.jpeg
new file mode 100644
index 0000000..c07bc98
Binary files /dev/null and b/docs/assets/img/screenshots/console.jpeg differ
diff --git a/docs/assets/img/screenshots/display.gif b/docs/assets/img/screenshots/display.gif
new file mode 100644
index 0000000..45b5d1a
Binary files /dev/null and b/docs/assets/img/screenshots/display.gif differ
diff --git a/docs/assets/img/screenshots/display_example.jpeg b/docs/assets/img/screenshots/display_example.jpeg
new file mode 100644
index 0000000..bd96227
Binary files /dev/null and b/docs/assets/img/screenshots/display_example.jpeg differ
diff --git a/docs/assets/img/screenshots/gpio.jpeg b/docs/assets/img/screenshots/gpio.jpeg
new file mode 100644
index 0000000..603fdc4
Binary files /dev/null and b/docs/assets/img/screenshots/gpio.jpeg differ
diff --git a/docs/assets/img/screenshots/ha_disco_1.jpeg b/docs/assets/img/screenshots/ha_disco_1.jpeg
new file mode 100644
index 0000000..6d700b9
Binary files /dev/null and b/docs/assets/img/screenshots/ha_disco_1.jpeg differ
diff --git a/docs/assets/img/screenshots/ha_disco_2.jpeg b/docs/assets/img/screenshots/ha_disco_2.jpeg
new file mode 100644
index 0000000..9d799c3
Binary files /dev/null and b/docs/assets/img/screenshots/ha_disco_2.jpeg differ
diff --git a/docs/assets/img/screenshots/hardware.jpeg b/docs/assets/img/screenshots/hardware.jpeg
new file mode 100644
index 0000000..4c0f437
Binary files /dev/null and b/docs/assets/img/screenshots/hardware.jpeg differ
diff --git a/docs/assets/img/screenshots/hardware_config.jpeg b/docs/assets/img/screenshots/hardware_config.jpeg
new file mode 100644
index 0000000..03b3670
Binary files /dev/null and b/docs/assets/img/screenshots/hardware_config.jpeg differ
diff --git a/docs/assets/img/screenshots/management.jpeg b/docs/assets/img/screenshots/management.jpeg
new file mode 100644
index 0000000..95e2ea8
Binary files /dev/null and b/docs/assets/img/screenshots/management.jpeg differ
diff --git a/docs/assets/img/screenshots/mqtt.jpeg b/docs/assets/img/screenshots/mqtt.jpeg
new file mode 100644
index 0000000..a4e589c
Binary files /dev/null and b/docs/assets/img/screenshots/mqtt.jpeg differ
diff --git a/docs/assets/img/screenshots/mqtt_explorer.jpeg b/docs/assets/img/screenshots/mqtt_explorer.jpeg
new file mode 100644
index 0000000..6faa64d
Binary files /dev/null and b/docs/assets/img/screenshots/mqtt_explorer.jpeg differ
diff --git a/docs/assets/img/screenshots/network.jpeg b/docs/assets/img/screenshots/network.jpeg
new file mode 100644
index 0000000..3de5b88
Binary files /dev/null and b/docs/assets/img/screenshots/network.jpeg differ
diff --git a/docs/assets/img/screenshots/output1.jpeg b/docs/assets/img/screenshots/output1.jpeg
new file mode 100644
index 0000000..e7334f4
Binary files /dev/null and b/docs/assets/img/screenshots/output1.jpeg differ
diff --git a/docs/assets/img/screenshots/output2.jpeg b/docs/assets/img/screenshots/output2.jpeg
new file mode 100644
index 0000000..1621d64
Binary files /dev/null and b/docs/assets/img/screenshots/output2.jpeg differ
diff --git a/docs/assets/img/screenshots/overview.jpeg b/docs/assets/img/screenshots/overview.jpeg
new file mode 100644
index 0000000..2bb7a43
Binary files /dev/null and b/docs/assets/img/screenshots/overview.jpeg differ
diff --git a/docs/assets/img/screenshots/pid_tuning.jpeg b/docs/assets/img/screenshots/pid_tuning.jpeg
new file mode 100644
index 0000000..c9b4b58
Binary files /dev/null and b/docs/assets/img/screenshots/pid_tuning.jpeg differ
diff --git a/docs/assets/img/screenshots/relays.jpeg b/docs/assets/img/screenshots/relays.jpeg
new file mode 100644
index 0000000..975e27a
Binary files /dev/null and b/docs/assets/img/screenshots/relays.jpeg differ
diff --git a/docs/assets/img/screenshots/remote-jsy-1.jpeg b/docs/assets/img/screenshots/remote-jsy-1.jpeg
new file mode 100644
index 0000000..697abe7
Binary files /dev/null and b/docs/assets/img/screenshots/remote-jsy-1.jpeg differ
diff --git a/docs/assets/img/screenshots/remote-jsy-2.jpeg b/docs/assets/img/screenshots/remote-jsy-2.jpeg
new file mode 100644
index 0000000..d5784da
Binary files /dev/null and b/docs/assets/img/screenshots/remote-jsy-2.jpeg differ
diff --git a/docs/assets/img/screenshots/shelly_script_id.jpeg b/docs/assets/img/screenshots/shelly_script_id.jpeg
new file mode 100644
index 0000000..b71934d
Binary files /dev/null and b/docs/assets/img/screenshots/shelly_script_id.jpeg differ
diff --git a/docs/assets/img/screenshots/shelly_solar_diverter.jpeg b/docs/assets/img/screenshots/shelly_solar_diverter.jpeg
new file mode 100644
index 0000000..11c483b
Binary files /dev/null and b/docs/assets/img/screenshots/shelly_solar_diverter.jpeg differ
diff --git a/docs/assets/img/screenshots/statistics.jpeg b/docs/assets/img/screenshots/statistics.jpeg
new file mode 100644
index 0000000..97f03ce
Binary files /dev/null and b/docs/assets/img/screenshots/statistics.jpeg differ
diff --git a/docs/assets/img/screenshots/update.jpeg b/docs/assets/img/screenshots/update.jpeg
new file mode 100644
index 0000000..020ba45
Binary files /dev/null and b/docs/assets/img/screenshots/update.jpeg differ
diff --git a/docs/blog.md b/docs/blog.md
new file mode 100644
index 0000000..2b28bfb
--- /dev/null
+++ b/docs/blog.md
@@ -0,0 +1,12 @@
+---
+layout: default
+title: Blog
+description: Blog
+---
+
+# Blog
+
+- [2024-07-01 - Shelly Solar Diverter / Router](/blog/2024-07-01_shelly_solar_diverter)
+- [2024-06-26 - Everything on le JSY](/blog/2024-06-26_everything_on_the_jsy)
+- [2024-06-25 - Remote JSY through UDP](/blog/2024-06-25_remote_jsy)
+- [2024-06-23 - Development is still in progress](/blog/2024-06-23_development_is_still_in_progress)
diff --git a/docs/blog/2024-06-23_development_is_still_in_progress.md b/docs/blog/2024-06-23_development_is_still_in_progress.md
new file mode 100644
index 0000000..8984077
--- /dev/null
+++ b/docs/blog/2024-06-23_development_is_still_in_progress.md
@@ -0,0 +1,17 @@
+---
+layout: default
+title: Blog
+description: Development is still in progress at a good pace
+---
+
+_Date: 2024-06-23_
+
+# Development is still in progress
+
+Development of YaSolR is still in progress at a good pace.
+You can follow the progress in the [GitHub project](https://github.com/mathieucarbou/YaSolR-OSS/projects?query=is%3Aopen) view.
+
+- The analysis is completed on real data to determine a good routing algorithm using a PID controller with a tweaked Kp, Ki and Kd in order to minimize the grid import and export as much as possible.
+- This PID algorithm will recompute each time a power change is detected, which means in a few milliseconds with a JSY.
+- The routing precision is high with a 12-bits resolution
+- The remote JSY feature through UDP is being implemented to facilitate testing
diff --git a/docs/blog/2024-06-25_remote_jsy.md b/docs/blog/2024-06-25_remote_jsy.md
new file mode 100644
index 0000000..068d247
--- /dev/null
+++ b/docs/blog/2024-06-25_remote_jsy.md
@@ -0,0 +1,41 @@
+---
+layout: default
+title: Blog
+description: Remote JSY through UDP
+---
+
+_Date: 2024-06-25_
+
+# Remote JSY
+
+The free [JSY library](https://oss.carbou.me/MycilaJSY/) has been completed with 2 new examples to show how to use the JSY remotely through UDP.
+
+The `Sender` program must be uploaded to an ESP32 connected to a JSY.
+It sends through UDP broadcast the JSY data several times per second.
+It can also be used as a standalone app to display the JSY data in real-time.
+
+![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0)
+
+There is also a `Listener` example which is the same app, bit not connected to the JSY, but will receive the data through UDP and display the metrics.
+
+The 2 samples are made with ESP-DASH, ElegantOTA, WebSerial, MycilaESpConnect, etc.
+
+=> [MycilaJSY RemoteUDP](https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP)
+=> [MycilaJSY](https://oss.carbou.me/MycilaJSY/) project
+
+## Remote JSY in YaSolR
+
+The YaSolR project also support the JSY `Sender`. YaSolR will listen for these UDP packets and will use the remote JSY data if they are available.
+
+You can have an ESP32 board installed in your electric box with a JSY and the `Sender` app, and YaSolr firmware installed next to the loads.
+
+When using a remote JSY in YaSolR, the following rules apply:
+
+- The voltage will always be read if possible from a connected JSY or PZEM, then from a remote JSY, then from MQTT.
+- The grid power will always be read first from MQTT, then from a remote JSY, then from a connected JSY.
+
+## Speed
+
+The JSY Remote through UDP is nearly as fast as having the JSY wired to the ESP.
+All changes to the JSY are immediately sent through UDP to the listener at a rate of about **20 messages per second**.
+This is the rate at which the JSY usually updates its data.
diff --git a/docs/blog/2024-06-26_everything_on_the_jsy.md b/docs/blog/2024-06-26_everything_on_the_jsy.md
new file mode 100644
index 0000000..4baf3b7
--- /dev/null
+++ b/docs/blog/2024-06-26_everything_on_the_jsy.md
@@ -0,0 +1,104 @@
+---
+layout: default
+title: Blog
+description: Everything on the JSY
+---
+
+_Date: 2024-06-26_
+
+# Everything on the JSY
+
+I am developing quite a few projects and librairies in the Arduino / ESP32 / Home automation landscape ([https://oss.carbou.me](https://oss.carbou.me)), including YaSolR routing software as well.
+
+Last year I created a specialized library for the JSY, which I wanted to talk to you about today in order to present its operation and use in the context of a solar router.
+
+## How the JSY is used in a solar router
+
+A solar router needs to measure the current, to react to these measurements and propose a new duty cycle to apply to the dimmer, for a certain time, until it obtains a new measurement.
+So it depends on the speed at which these measurements are taken, and their precision, but also of course on the routing algorithm used and the precision of the steps (0-100, 8-bit, 12-bit, etc.).
+
+It's rare to find a solar router that correctly uses JSY at its full capacity, partly due to the severe lack of an effective library for JSY, and that's why I created it.
+
+## Asynchronous operation
+
+As JSY slows down routing, it is important to be able to retrieve its values ​​as quickly as possible.
+Most libraries use delays or read() in loops, which is non-blocking, good, but does not guarantee reading the data as soon as it is ready because these read tests depend on the speed of execution of the loop.
+
+Not many people know this, but Arduino offers a serial read method (Serial.readBytes()), which is implemented differently from the non-blocking read(): readBytes() is blocking and directly uses the UART interrupts backwards to unlock as soon as the data is ready.
+This is the most effective method to be notified immediately of the availability of measurements.
+It is then enough to set up a reading loop in an asynchronous task on core 0 of the ESP32, to be notified as soon as the data is ready.
+
+## JSY reading speeds
+
+The JSY speed can be changed from its default (4800 bps) to 9600, 19200 and 38400 bps.
+
+- At 4800 bps, the JSY takes on average 171 ms to make a reading
+- At 9600 bps, the JSY takes on average 100 ms to make a reading
+- At 19200 bps, the JSY takes on average 60 ms to take a reading
+- At 38400 bps, the JSY takes on average 41 ms to take a reading
+
+So increasing the speed of your JSY from 4800 to 38400 potentially allows you to react 4x faster to these readings...
+But are these readings meaningful?
+Let's see...
+
+## Internal workings of JSY
+
+The JSY works with an Renergy RN8209G chip, which continuously measures by taking a rolling average and makes the results available on the UART.
+For example. if you read the JSY repeatedly:
+
+- At 4800 bps, the JSY reports on average one change every 3 readings, or 513 ms
+- At 9600 bps, the JSY reports on average a change every 4-5 readings, or 400-500 ms
+- At 19200 bps, the JSY reports on average one change every 9 readings, or 360 ms
+- At 38400 bps, the JSY reports on average one change every 9 readings, or 369 ms
+
+So it is not possible to have a router that will make a correction to the routing faster than this minimum delay for the JSY to detect a change in the measurements.
+So the routing algorithm should apply for at least 300 ms.
+
+## Load Detection Time
+
+The JSY has a load detection time.
+For example, when turning on a load of 0-100%, it takes a certain amount of time to start making its data available.
+
+- At 4800 bps, the JSY takes approximately 680 ms to detect a load turning on
+- At 9600 bps, the JSY takes approximately 400-700 ms to detect a load turning on
+- At 19200 bps, the JSY takes approximately 360 ms to detect a load turning on
+- At 38400 bps, the JSY takes approximately 330 ms to detect a load turning on
+
+This is the minimum time that the JSY takes to make a measurement available after a load change.
+
+## Ramp-up Time
+
+When a load is on from 0 to 100%, goes from 0 to 3000W for example, the JSY takes time to see the nominal power because it uses moving average.
+
+- At 4800 bps, the JSY has a ramp-up time (time before seeing nominal power) of 1198 ms
+- At 9600 bps, JSY has a ramp-up time of 800-1101 ms
+- At 19200 bps, JSY has a ramp-up time of 1020 ms
+- At 38400 bps, the JSY has a ramp-up time of 1030 ms
+
+This is pretty consistent, and it's the JSY window duration, which is about 1 second.
+
+## Using JSY in remote mode
+
+The library includes a `Sender` application and another `Listener`.
+The sender application is a standalone application to flash on an ESP32 connected to a JSY.
+It uses ESP-DASH, ElegantOTA, MycilaESPConnect, etc. and allows you to see all the JSY stats, reset the energy, and, above all, sends the measurements via UDP at a **speed of 20 messages per second**, which is as fast as than reading the data locally at 38400 bps on an JSY connected to the ESP32.
+The `Listener` application shows how to receive them at a processing speed of 20 messages per second.
+
+![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0)
+
+## Conclusion
+
+- Increasing the speed of the JSY does not serve to read more measurements, but above all serves to be notified as quickly as possible when new measurements arrive, without having to wait for the loop to return.
+- The JSY appears to have a sliding window of 1 second, and appears to make its measurements available every 300 ms
+- Whether the JSY is connected to the ESP or whether the data is retrieved remotely by UDP with the `Sender` application does not change the speed or precision of routing
+
+## MycilaJSY Library
+
+The library is here, with performance tests based on speeds: [https://github.com/mathieucarbou/MycilaJSY](https://github.com/mathieucarbou/MycilaJSY)
+
+It supports:
+- Non-blocking mode with asynchronous task
+- Callbacks to be notified of metric readings and changes
+- Energy reset
+- Gear switch
+- Remote operation via UDP at a speed of 20 messages per second
diff --git a/docs/blog/2024-07-01_shelly_solar_diverter.md b/docs/blog/2024-07-01_shelly_solar_diverter.md
new file mode 100644
index 0000000..2429840
--- /dev/null
+++ b/docs/blog/2024-07-01_shelly_solar_diverter.md
@@ -0,0 +1,360 @@
+---
+layout: default
+title: Blog
+description: "Shelly Solar Diverter / Router: redirects the excess solar production to a water tank or heater"
+---
+
+_Date: 2024-07-01_
+
+_I've put the YaSolR project in pause for a few days to work on this very cool and awesome Shelly integration..._
+
+# Shelly Solar Diverter / Router
+
+| [![](../assets/img/hardware/shelly_solar_diverter_poc2.jpeg)](../assets/img/hardware/shelly_solar_diverter_poc2.jpeg) | [![](../assets/img/screenshots/shelly_solar_diverter.jpeg)](../assets/img/screenshots/shelly_solar_diverter.jpeg) |
+
+- [What is a Solar Router / Diverter ?](#what-is-a-solar-router--diverter-)
+- [Shelly Solar Diverter](#shelly-solar-diverter-1)
+- [Download](#download)
+- [Hardware](#hardware)
+- [Wiring](#wiring)
+ - [Shelly Add-On + DS18B20](#shelly-add-on--ds18b20)
+ - [Electric Circuit](#electric-circuit)
+ - [RC Snubber](#rc-snubber)
+ - [Add other dimmers](#add-other-dimmers)
+- [Setup](#setup)
+ - [Shelly Dimmer Setup](#shelly-dimmer-setup)
+ - [Shelly Pro EM 50 Setup](#shelly-pro-em-50-setup)
+- [How to use](#how-to-use)
+ - [Configuration](#configuration)
+ - [Several dimmers](#several-dimmers)
+ - [Excess sharing amongst dimmers](#excess-sharing-amongst-dimmers)
+ - [Start / Stop Automatic Divert](#start--stop-automatic-divert)
+ - [Solar Diverter Status](#solar-diverter-status)
+ - [PID Control and Tuning](#pid-control-and-tuning)
+- [Future Improvements](#future-improvements)
+- [Demos](#demos)
+- [Help and Support](#help-and-support)
+
+## What is a Solar Router / Diverter ?
+
+A _Solar Router_ allows to redirect the solar production excess to some appliances instead of returning it to the grid.
+The particularity of a solar router is that it will dim the voltage and power sent to the appliance in order to match the excess production, in contrary to a simple relay that would just switch on/off the appliance without controlling its power.
+
+A _Solar Router_ is usually connected to the resistance of a water tank and will heat the water when there is production excess.
+
+A solar router can also do more things, like controlling (on/off) the activation of other appliances (with the grid normal voltage and not the dimmed voltage) in case the excess reaches a threshold. For example, one could activate a pump, pool heater, etc if the excess goes above a specific amount, so that this appliance gets the priority over heating the water tank.
+
+A router can also schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage. This is called a bypass relay.
+
+## Shelly Solar Diverter Features
+
+- **Unlimited dimmers (output)**
+- **PID Controller**
+- **Excess sharing amongst dimmers with percentages**
+- **Bypass (force heating)** and automatically turn the dimmer off
+- **Plus all the power of the Shelly ecosystem (rules, schedules, automations, etc)**
+
+This solar diverter based on Shelly devices and a Shelly script can control remotely dimmers and could even be enhanced with relays.
+Shelly's being remotely controllable, such system offers a very good integration with Shelly App and Home Automation Systems like Home Assistant.
+
+It is possible to put some rules based on temperature, time, days, etc and control everything from the Shelly App or Home Assistant.
+
+The Shelly script, when activated, automatically adjusts the dimmers to the grid import or export (solar production excess).
+
+## Download
+
+- **[Shelly Solar Diverter Script](../downloads/auto_diverter_v1.js)**
+
+## Hardware
+
+All the components can be bought at [https://www.shelly.com/](https://www.shelly.com/), except the voltage regulator, where you can find some links [on my website](../build#voltage-regulators)
+
+| [Shelly Pro EM - 50](https://www.shelly.com/fr/products/shop/proem-1x50a) | [Shelly Dimmer 0/1-10V PM Gen3](https://www.shelly.com/fr/products/shop/1xsd10pmgen3) | [Shelly Plus Add-On](https://www.shelly.com/fr/products/shop/shelly-plus-add-on) | [Temperature Sensor DS18B20](https://www.shelly.com/fr/products/shop/temperature-sensor-ds18B20) | Voltage Regulator
- [Loncont LSA-H3P50YB](https://fr.aliexpress.com/item/32606780994.html)
- [LCTC DTY-220V40P1](https://fr.aliexpress.com/item/1005005008018888.html) |
+| :-----------------------------------------------------------------------: | :-----------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| ![](../assets/img/hardware/Shelly_Pro_EM_50.jpeg) | ![](../assets/img/hardware/Shelly_Dimmer-10V.jpeg) | ![](../assets/img/hardware/Shelly_Addon.jpeg) | ![](../assets/img/hardware/Shelly_DS18.jpeg) | ![](../assets/img/hardware/LSA-H3P50YB.jpeg)
![](../assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg) |
+
+Some additional hardware are required depending on the installation.
+**Please select the amperage according to your needs.**
+
+- A 2A breaker for the Shelly electric circuit
+- A 16A or 20A breaker for your water tank (resistance) electric circuit
+- A 25A relay or contactor for the bypass relay (to force a heating) for the water tank electric circuit
+- A protection box for the Shelly
+
+## Wiring
+
+### Shelly Add-On + DS18B20
+
+First the easy part: the temperature sensor and the Shelly Add-On, which has to be put behind the Shelly Dimmer.
+
+| [![](../assets/img/hardware/Shelly_Addon_DS18.jpeg)](../assets/img/hardware/Shelly_Addon_DS18.jpeg) | [![](../assets/img/hardware/shelly_dimmer_with_addon.jpeg)](../assets/img/hardware/shelly_dimmer_with_addon.jpeg) |
+
+### Electric Circuit
+
+- Choose your breakers and wires according to your load
+- Circuits can be split.
+ For example, the Shelly EM can be inside the main electric box, and the Shelly Dimmer + Add-On can be in the water tank electric panel, while the contactor and dimmer can be placed neat the water tank.
+ They communicate through the network.
+- The dimmer will control the voltage regulator through the `COM` and `0-10V` ports
+- The Shelly EM will control the relay or contactor through the `A2` ports.
+- The B clamp around the wire going from the voltage regulator to the water tank is to measure the current going through the water tank resistance is optional and for information purposes only.
+- The A clamp should be put around the main phase entering the house
+- The relay / contactor is optional and is used to schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage.
+- The neutral wire going to the voltage regulator can be a small one; it is only used for the voltage and Zero-Crossing detection.
+- Communication is done through WiFI: **make sure you have a good WiFi to reduce the connection time and improve the router speed.**
+
+[![](../assets/img/schemas/Solar_Router_Diverter.jpg)](../assets/img/schemas/Solar_Router_Diverter.jpg)
+
+### RC Snubber
+
+If switching the contactor / relay causes the Shelly device to reboot, place a [RC Snubber](https://www.shelly.com/fr/products/shop/shelly-rc-snubber) between the A1 and A2 ports of the contactor / relay.
+
+### Add other dimmers
+
+If you want to control a second resistive load, it is possible to duplicate the circuit to add another dimmer and voltage regulator.
+
+Modify the config accordingly to support many dimmers and they will be turned on/off sequentially to match the excess production.
+
+## Setup
+
+First make sure that your Shelly's are setup properly.
+
+The script has to be installed inside the Shelly Pro EM 50, because this is where the measurements of the imported and exported grid power is done.
+Also, this central place allows to control the 1, 2 or more dimmers remotely.
+
+### Shelly Dimmer Setup
+
+- Set static IP addresses
+- Use the min/max settings to remap the 0-100% to match the voltage regulator. In the case of the LSA and LCTC, I found that **I had to remap to 10%-80%.**
+
+### Shelly Pro EM 50 Setup
+
+- Set static IP address
+- Make sure to place the A clamp around the main phase entering the house in the right direction
+- Add the `Shelly Solar Diverter` script to the Shelly Pro EM
+- Configure the settings in the `CONFIG` object
+- Start the script
+- Activate `Run on startup`
+
+## How to use
+
+### Configuration
+
+Edit the `CONFIG` object and pay attention to the values, especially the resistance value which should be accurate, otherwise the routing precision will be bad.
+
+```javascript
+const CONFIG = {
+ // Debug mode
+ DEBUG: 1,
+ // Grid Power Read Interval (s)
+ READ_INTERVAL_S: 1,
+ PID: {
+ // Reverse
+ REVERSE: false,
+ // Proportional Mode:
+ // - "error" (proportional on error),
+ // - "input" (proportional on measurement),
+ // - "both" (proportional on 50% error and 50% measurement)
+ P_MODE: "input",
+ // Derivative Mode:
+ // - "error" (derivative on error),
+ // - "input" (derivative on measurement)
+ D_MODE: "error",
+ // Integral Correction
+ // - "off" (integral sum not clamped),
+ // - "clamp" (integral sum not clamped to OUT_MIN and OUT_MAX),
+ // - "advanced" (advanced anti-windup algorithm)
+ IC_MODE: "advanced",
+ // Target Grid Power (W)
+ SETPOINT: 0,
+ // PID Proportional Gain
+ KP: 0.3,
+ // PID Integral Gain
+ KI: 0.3,
+ // PID Derivative Gain
+ KD: 0.1,
+ // Output Minimum (W)
+ OUT_MIN: -10000,
+ // Output Maximum (W)
+ OUT_MAX: 10000,
+ },
+ DIMMERS: {
+ "192.168.125.98": {
+ // Resistance (in Ohm) of the load connecter to the dimmer + voltage regulator
+ // 0 will disable the dimmer
+ RESISTANCE: 24,
+ // Percentage of the remaining excess power that will be assigned to this dimmer
+ // The remaining percentage will be given to the next dimmers
+ RESERVED_EXCESS_PERCENT: 100,
+ // Set whether the Shelly EM with this script will be used to control the bypass relay to force a heating
+ // When set to true, if you activate remotely the bypass to force a heating, then the script will detect it and turn the dimmer off
+ BYPASS_CONTROLLED_BY_EM: true,
+ },
+ "192.168.125.97": {
+ RESISTANCE: 0,
+ RESERVED_EXCESS_PERCENT: 100,
+ BYPASS_CONTROLLED_BY_EM: false,
+ },
+ },
+};
+```
+
+### Several dimmers
+
+The script can automatically control several dimmers.
+Just add the IP address of the dimmer and the resistance of the load connected to it.
+
+**How it works:**
+
+If you have 2000W of excess, and 2 dimmers of 1500W each (nominal load), then the first ome will be set at 100% and will consume 1500W and the second one will consume the remaining 500W.
+
+### Excess sharing amongst dimmers
+
+It is possible to share the excess power amongst the dimmers.
+Let's say you have 3 dimmers with this configuration:
+
+```javascript
+DIMMERS: {
+ "192.168.125.93": {
+ RESISTANCE: 53,
+ RESERVED_EXCESS_PERCENT: 50,
+ },
+ "192.168.125.94": {
+ RESISTANCE: 53,
+ RESERVED_EXCESS_PERCENT: 25,
+ },
+ "192.168.125.95": {
+ RESISTANCE: 53,
+ RESERVED_EXCESS_PERCENT: 100,
+ }
+}
+```
+
+When you'll have 3000W of excess:
+
+- the first one will take up to 50% of it (1500 W), but it will only take 1000 W because of the resistance. So 2000 W remains.
+- the second one 25% of teh remaining (500 W)
+- the third one will take the remaining 1000 W
+
+### Start / Stop Automatic Divert
+
+Once the script is uploaded and started, it will automatically manage the power sent to the resistive load according to the rules above.
+
+You can start / stop the script manually from the interface or remotely by calling:
+
+```
+http://192.168.125.92/rpc/Script.Start?id=1
+http://192.168.125.92/rpc/Script.Stop?id=1
+```
+
+- `192.168.125.92` begin the Shelly EM 50 static IP address.
+- `1` being the script ID as seen in the Shelly interface
+
+![](../assets/img/screenshots/shelly_script_id.jpeg)
+
+### Solar Diverter Status
+
+You can view the status of the script by going to the script `status` endpoint, which is only available when the script is running.
+
+```
+http://192.168.125.92/script/1/status
+```
+
+```json
+{
+ "config": {
+ "DEBUG": 2,
+ "READ_INTERVAL_S": 1,
+ "PID": {
+ "REVERSE": false,
+ "P_MODE": "input",
+ "D_MODE": "error",
+ "IC_MODE": "advanced",
+ "SETPOINT": 0,
+ "KP": 0.3,
+ "KI": 0.3,
+ "KD": 0.1,
+ "OUT_MIN": -10000,
+ "OUT_MAX": 10000
+ },
+ "DIMMERS": {
+ "192.168.125.98": {
+ "RESISTANCE": 24,
+ "RESERVED_EXCESS_PERCENT": 100
+ },
+ "192.168.125.97": {
+ "RESISTANCE": 0,
+ "RESERVED_EXCESS_PERCENT": 100
+ }
+ }
+ },
+ "pid": {
+ "input": 0,
+ "output": 0,
+ "error": 0,
+ "pTerm": 0,
+ "iTerm": 0,
+ "dTerm": 0,
+ "sum": 0
+ },
+ "divert": {
+ "lastTime": 1720281498691.629,
+ "dimmers": {
+ "192.168.125.98": {
+ "divertPower": 0,
+ "maximumPower": 2263.98374999999,
+ "dutyCycle": 0,
+ "powerFactor": 0,
+ "dimmedVoltage": 0,
+ "current": 0,
+ "apparentPower": 0,
+ "thdi": 0,
+ "rpc": "success"
+ }
+ }
+ }
+}
+```
+
+### Automation ideas
+
+- Stop the automatic divert when the temperature of the water tank reaches a specific value, and turn it back on when the temperature goes below a specific value.
+- Schedule a force heating of the water tank based on days and hours
+ - Either by turning on the bypass relay controlled by the Shelly EM
+ - Or by disabling the auto-divert script, and then turning the dimmer on and set it to 100%
+
+You may need to use Home Assistant or Jeedom depending on what you need to do because the Shelly App, at the time of writing, does not support a lot of actions.
+
+### PID Control and Tuning
+
+The script uses a complex PID controller that can be tuned to really obtain a very good routing precision.
+The algorithm used and default parameters are the same as in the YaSolr project.
+You will find a lot of information in the [YaSolR manual](/manual#pid-controller-section).
+
+## Future Improvements
+
+- Key Value storage to store the configuration and status of the script
+- Virtual Components to expose script variables (also soon in [Home Assistant](https://github.com/home-assistant/core/pull/119932))
+
+## Demos
+
+Here is a demo video of the Shelly device reacting to EM measured power:
+
+[![Shelly Solar Diverter Demo](https://img.youtube.com/vi/qDV0VZnWXWU/0.jpg)](https://www.youtube.com/watch?v=qDV0VZnWXWU "Shelly Solar Diverter Demo")
+
+Here is a demo video where you can see the solar production and grid power of the home, and the Shelly device reacting to the excess production and setpoint defined.
+
+[![Shelly Solar Diverter Demo](https://img.youtube.com/vi/cOfrMYWf8tY/0.jpg)](https://www.youtube.com/watch?v=cOfrMYWf8tY "Shelly Solar Diverter Demo")
+
+Here is a PoC box I am using for my testing with all the components wired.
+In this PoC on the left, I have used the LCTC voltage regulator which comes already pre-mounted on a heat sink.
+On the right, with the LSA.
+The Shelly Dimmer Gen 3 with the Shelly Addon are in the black enclosure, the voltage regulator on the right and the Shelly EM Pro at the top right.
+
+| [![](../assets/img/hardware/shelly_solar_diverter_poc.jpeg)](../assets/img/hardware/shelly_solar_diverter_poc.jpeg) | [![](../assets/img/hardware/shelly_solar_diverter_poc2.jpeg)](../assets/img/hardware/shelly_solar_diverter_poc2.jpeg) |
+
+## Help and Support
+
+- [Forum photovoltaĂŻque](https://forum-photovoltaique.fr/viewtopic.php?t=72838)
+- [Discussions](https://github.com/mathieucarbou/YaSolR-OSS/discussions) (on GitHub)
+- [Issues](https://github.com/mathieucarbou/YaSolR-OSS/issues) (on GitHub)
diff --git a/docs/build.md b/docs/build.md
new file mode 100644
index 0000000..b00bcec
--- /dev/null
+++ b/docs/build.md
@@ -0,0 +1,486 @@
+---
+layout: default
+title: Build
+description: Build
+---
+
+# How to build your router
+
+- [Build Examples](#build-examples)
+ - [The Recycler](#the-recycler)
+ - [The Minimalist](#the-minimalist)
+ - [The Adventurer](#the-adventurer)
+ - [The Elite](#the-elite)
+ - [The Professional (🚧)](#the-professional-%F0%9F%9A%A7)
+ - [Possible Upgrades](#possible-upgrades)
+ - [Remote JSY](#remote-jsy)
+ - [Alternative: The Shelly Solar Diverter](#alternative-the-shelly-solar-diverter)
+- [Selecting your Hardware](#selecting-your-hardware)
+ - [ESP32 Boards](#esp32-boards)
+ - [Dimmers: Robodyn, Solid State Relay or Voltage Regulator ?](#dimmers-robodyn-solid-state-relay-or-voltage-regulator-)
+ - [Relays: Solid State Relay or Electromagnetic Relay ?](#relays-solid-state-relay-or-electromagnetic-relay-)
+ - [How to choose a Solid State Relay ?](#how-to-choose-a-solid-state-relay-)
+- [Where to buy ?](#where-to-buy-)
+ - [ESP32 Boards](#esp32-boards-1)
+ - [Robodyn](#robodyn)
+ - [Random and Zero-Cross SSR](#random-and-zero-cross-ssr)
+ - [Voltage Regulators](#voltage-regulators)
+ - [Electromagnetic Relay](#electromagnetic-relay)
+ - [Measurement Devices](#measurement-devices)
+ - [Temperature Sensors, LEDs, Buttons, Displays](#temperature-sensors-leds-buttons-displays)
+ - [Mounting Accessories](#mounting-accessories)
+- [Default GPIO pinout per board](#default-gpio-pinout-per-board)
+- [Pictures of some routers](#pictures-of-some-routers)
+
+## Build Examples
+
+YaSolR supports many builds and routing algorithms.
+Before building your router, you need to decide which type of hardware you want to use.
+Here are below some examples:
+
+- [The Recycler](#the-recycler): reuse your existing Shelly EM or Shelly 3EM to build a router
+- [The Minimalist](#the-minimalist): the cheapest and easiest to build
+- [The Adventurer](#the-adventurer): for people who want to mitigate the flaws of the Robodyn and do some improvements over the existing Robodyn
+- [The Elite](#the-elite): for people who want to use a Random SSR instead of a Robodyn to safely dim more power and have a better Zero-Cross Detection circuit
+- [The Professional (🚧)](#the-professional-%F0%9F%9A%A7): probably the best and safe solution out there but requires an additional power source
+- [Possible Upgrades](#possible-upgrades): some additional components you can add to your router
+- [Remote JSY](#remote-jsy): a standalone application to place in your electrical panel to send the JSY metrics through UDP for remote installations
+- [Alternative: The Shelly Solar Diverter](#alternative-the-shelly-solar-diverter): a limited Solar Diverter / Router with Shelly devices and a voltage regulator
+
+### The Recycler
+
+Reuse your existing Shelly EM or Shelly 3EM to build a router!
+
+| ESP32-DevKitC | Robodyn AC Dimmer 40A/800V | Shelly EM or 3EM |
+| :----------------------------------------------------------------------: | :--------------------------------------------------------------------: | :-----------------------------------------------------------------: |
+| | | |
+
+> ##### TIP
+>
+> - Robodyn includes Zero-Cross Detection circuit
+> - Supports **Phase Control** and **Burst mode**
+> - Reuse your Shelly EM or 3EM and send through MQTT grid power and voltage
+> - You can also use a SSR and ZCD module instead of the Robodyn
+> {: .block-tip }
+
+> ##### WARNING
+>
+> - Advised load not more than 2000W
+> - Robodyn has poor quality heat sink, soldering and Zero-Cross pulse
+> - Bypass mode will use the Robodyn dimmer at 100% power
+> - Not as precise as a JSY (MQTT delays)
+> - No local measurement in place to measure the routed power (statistics will be empty)
+> {: .block-warning }
+
+### The Minimalist
+
+The _Minimalist_ build uses inexpensive and easy to use components to start a router.
+
+| ESP32-DevKitC | Robodyn AC Dimmer 40A/800V | JSY-MK-194T with 2 remote clamps |
+| :----------------------------------------------------------------------: | :--------------------------------------------------------------------: | :----------------------------------------------------------------------: |
+| | | |
+
+> ##### TIP
+>
+> - Robodyn includes Zero-Cross Detection circuit
+> - Supports **Phase Control** and **Burst mode**
+> {: .block-tip }
+
+> ##### WARNING
+>
+> - Advised load not more than 2000W
+> - Robodyn has poor quality heat sink, soldering and Zero-Cross pulse
+> - Bypass mode will use the Robodyn dimmer at 100% power
+> {: .block-warning }
+
+### The Adventurer
+
+The _Adventurer_ build is for people who are able to mitigate the flaws of the Robodyn 24A to improve it.
+The TRIAC can be changed to a BTA40-800B RD91 fixed directly on the heat sink, and the heat sink can be upgraded.
+See the [Robodyn](#robodyn) section for more information.
+
+| ESP32-DevKitC | Robodyn AC Dimmer 24A/600V | Heat Sink | Triac BTA40-800B RD91 | JSY-MK-194T with 2 remote clamps |
+| :----------------------------------------------------------------------: | :--------------------------------------------------------------------: | :------------------------------------------------------------------: | :-------------------------------------------------------------------: | ------------------------------------------------------------------------ |
+| | | | | |
+
+> ##### TIP
+>
+> - Robodyn includes Zero-Cross Detection circuit
+> - Supports **Phase Control** and **Burst mode**
+> {: .block-tip }
+
+> ##### WARNING
+>
+> - Advised load not more than 2000W
+> - Robodyn has poor quality heat sink, soldering and Zero-Cross pulse
+> - Bypass mode will use the Robodyn dimmer at 100% power
+> - Requires to unsolder the heat sink and triac and put a new triac on a new heat sink
+> {: .block-warning }
+
+### The Elite
+
+The _Elite_ build is for people who want to use a Random SSR instead of a Robodyn to safely dim more power and have a better Zero-Cross Detection circuit more more precising routing.
+
+| ESP32-DevKitC | Random Solid State Relay | Heat Sink | Zero-Cross Detection Module | JSY-MK-194T with 2 remote clamps |
+| :----------------------------------------------------------------------: | :-------------------------------------------------------------------: | :------------------------------------------------------------------: | :------------------------------------------------------------: | ------------------------------------------------------------------------ |
+| | | | | |
+
+> ##### TIP
+>
+> - Dedicated ZCD circuit with a good pulse
+> - Dedicated Random SSR (models up to 100A)
+> - Supports **Phase Control** and **Burst mode**
+> - Other types of Heat Sink are available: the image above is just an example. Pick one according to your load.
+> - All the components can be easily attached onto a DIN rail
+> {: .block-tip }
+
+> ##### WARNING
+>
+> - Bypass mode will use the SSR dimmer set at 100% power
+> {: .block-warning }
+
+### The Professional (🚧)
+
+The _Professional_ build uses a Voltage Regulator to control the power routing.
+This is probably the best reliable and efficient solution, but it is more complex to setup and wire.
+It requires an additional 12V power supply.
+
+| ESP32-DevKitC | Voltage Regulator | Heat Sink | PWM to Analog Converter | JSY-MK-194T with 2 remote clamps |
+| :----------------------------------------------------------------------: | :--------------------------------------------------------------------: | :----------------------------------------------------------------------: | :--------------------------------------------------------------------: | ------------------------------------------------------------------------ |
+| | | | | |
+
+> ##### TIP
+>
+> - Dedicated hardware supporting high loads
+> - Supports **Phase Control** and **Burst mode**
+> - Heat sink are bigger and better quality: bigger models are also available
+> - All the components can be easily attached onto a DIN rail
+> {: .block-tip }
+
+> ##### WARNING
+>
+> - Requires an additional 12V power supply (i.e. Mean Well HDR-15-15 12V DC)
+> - Bypass mode will use the dimmer set at 100% power
+> {: .block-warning }
+
+### Possible Upgrades
+
+Here are below what you can add to upgrade your router:
+
+| Hardware | Description |
+| :--------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------ |
+| | A bypass relay to avoid using the dimmer when auto bypass is enabled, and an additional relay to control an external load |
+| | A temperature sensor to measure the water tank temperature to automatically stop or start the water heating |
+| | A push button to restart the router easily |
+| | LEDs to display the system status |
+| | A display to show the router information |
+| | A PZEM to precisely measure the routed power for each output. Only useful if you have more than one output. |
+
+### Remote JSY
+
+Here are the components below to build a remote JSY and install the [Sender](https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP) .ino file on a it.
+This is a standalone application that looks looks like this and will show all your JSY data, help you manage it, and also send the data through UDP **at a rate of 20 messages per second**.
+
+![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0)
+
+You can look in the [JSY project](https://oss.carbou.me/MycilaJSY/) to find more information about how to setup remote JSY and the supported protocols.
+
+| Mean Well HDR-15-5 5V DC | ESP32-DevKitC | JSY-MK-194T with 2 remote clamps |
+| :--------------------------------------------------------------------: | :----------------------------------------------------------------------: | ------------------------------------------------------------------------ |
+| | | |
+
+### Alternative: The Shelly Solar Diverter
+
+It is also possible to build a (limited) Solar Diverter / Router with Shelly devices and a voltage regulator.
+
+| [![](./assets/img/hardware/shelly_solar_diverter_poc2.jpeg)](./assets/img/hardware/shelly_solar_diverter_poc2.jpeg) | [![](./assets/img/screenshots/shelly_solar_diverter.jpeg)](./assets/img/screenshots/shelly_solar_diverter.jpeg) |
+
+See this blog post for more information: [Shelly Solar Diverter](./blog/2024-07-01_shelly_solar_diverter)
+
+## Selecting your Hardware
+
+### ESP32 Boards
+
+Here are the boards we know are compatible and those we have tested.
+
+| **Board** | **UARTs** | **Ethernet** |
+| :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------: | :----------: |
+| [ESP32-DevKitC](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-devkitc.html) (recommended - 3 UARTs) | 3 | |
+| [ESP32-S3-DevKitC-1](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html) (recommended - 3 UARTs) | 3 | |
+| [T-ETH-Lite ESP32 S3](https://www.lilygo.cc/products/t-eth-lite?variant=43120880779445) (recommended - 3 UARTs) | 3 | âś… |
+| [ESP-32S](https://www.es.co.th/Schemetic/PDF/ESP32.PDF) | 2 | |
+| [WT32-ETH01](https://en.wireless-tag.com/product-item-2.html) | 2 | âś… |
+
+- _Compile_ means a firmware for this board can at least be built and flashed.
+- _Tested_ means someone has verified that firmware is working or partially working on this board.
+
+### Dimmers: Robodyn, Solid State Relay or Voltage Regulator ?
+
+Here are some pros and cons of each phase control system:
+
+**Robodyn (TRIAC):**
+
+- Pros:
+ - cheap and easy to wire
+ - 40A model comes with a heat sink and fan
+ - All in one device: phase control, ZCD, heat sink, fan
+- Cons:
+ - limited in load to 1/3 - 1/2 of the announced load
+ - 16A / 24A models comes with heat sink which is too small for its supported maximum load
+ - no solution ready to attach them on a DIN rail.
+ - The heat sink often has to be upgraded, except for the one on the 40 model which is already good for small loads below 2000W.
+ - The ZCD circuit [is less accurate](https://github.com/fabianoriccardi/dimmable-light/wiki/About-dimmer-boards) and pulses can be harder to detect [on some boards](https://github.com/fabianoriccardi/dimmable-light/wiki/Notes-about-specific-architectures#interrupt-issue)
+ - You need to go over some modifications to ([improve wiring / soldering and heat sink](https://sites.google.com/view/le-professolaire/routeur-professolaire))
+ - You might need to replace the Triac or move it
+
+**Solid State Relays:**
+
+- Pros:
+ - cheap and easy to wire
+ - support higher loads
+ - can be attached to a DIN rail with standard SSR clips
+ - lot of heat sink models available
+- Cons:
+ - limited in load to 1/3 - 1/2 of the announced load
+ - require an external ZCD module, heat sink and/or fan
+
+**Voltage Regulators:**
+
+Voltage regulators include a ZCD module and a phase control system which can be controlled in many ways.
+These are the best option, but complex to setup, wire and calibrate and require an additional 12V power supply.
+
+This is the option used in the [Shelly Solar Router](./blog/2024-07-01_shelly_solar_diverter).
+
+**Heat Sink:**
+
+In any case, do not forget to correctly dissipate the heat of your Triac / SSR using an appropriate heat sink.
+Robodyn heat sink is not enough and require some tweaking (like adding a flan or de-soldering the triac and heat sink and put the triac on a bigger heat sink).
+
+It is best to take a vertical heat sink for heat dissipation.
+In case of the Robodyn 40A, you can install it vertically.
+
+### Relays: Solid State Relay or Electromagnetic Relay ?
+
+**For bypass relays used for outputs**
+
+- For bypass relays, you can use electromagnetic relays because they will be used less frequently.
+ Also, some electromagnetic relays boards have both a NO and NC output to better isolate the dimming circuit and bypass circuit.
+
+**For external Relays**
+
+- If you want to use the relays to automatically switch one of the resistance of the water tank, as described in the [recommendations to reduce harmonics and flickering](./overview#recommendations-to-reduce-harmonics-and-flickering), you must use a SSR because the relay will be switched on and off frequently.
+- If you are not using the automatic relay switching, and you either control them manually or through a Home Automation system, you can use electromagnetic relays, providing the relays won't be switched on and off frequently.
+- Use a Zero-Cross SSR for resistive loads
+- Use a Random SSR for inductive loads (pump, motors)
+
+**Also to consider:**
+
+- You should not use electromagnetic relays to switch a load on and off frequently because they have a limited number of cycles before they fail and they can be stuck.
+- Relays have to be controllable through a 3.3V DC signal.
+- It is easier to find SSR supporting high loads that can be controlled by a 3.3V DC signal than electromagnetic relays.
+- Also, SSR with a DIN Rail clip are easy to install.
+- On the other hand, SSR can be more affected by harmonics than electromagnetic relays and they are more expensive.
+
+### How to choose a Solid State Relay ?
+
+- Make sure you add a **heat sink** to the SSR or pick one with a heat sink, especially if you use a Random SSR instead of a Robodyn
+- **Type of control**: DA: (DC Control AC)
+- **Control voltage**: 3.3V should be in the range (example: 3-32V DC)
+- Verify that the **output AC voltage** is in the range (example: 24-480V AC)
+- Verify the **SSR amperage**: usually, it should be 2-3 times the nominal current of your resistive load (example: 40A SSR for a 3000W resistance).
+ For induction loads, it should be 4-7 times the nominal current.
+- **Zero Cross SSR** (which is the default for most SSR): for the bypass relay or external relays with resistive loads, or when using Burst modulation routing algorithm
+- **Random SSR**: if you chose to not use the Robodyn but a Random SSR for Phase Control, or external relays with inductive loads (pump, motors)
+
+## Where to buy ?
+
+Here is the non exhaustive list where to find some hardware to build your router.
+Links are provided for reference only, you can find them on other websites.
+
+- [ESP32 Boards](#esp32-boards-1)
+- [Robodyn](#robodyn)
+- [Random and Zero-Cross SSR](#random-and-zero-cross-ssr)
+- [Voltage Regulators](#voltage-regulators)
+- [Electromagnetic Relay](#electromagnetic-relay)
+- [Measurement Devices](#measurement-devices)
+- [Temperature Sensors, LEDs, Buttons, Displays](#temperature-sensors-leds-buttons-displays)
+- [Mounting Accessories](#mounting-accessories)
+
+### ESP32 Boards
+
+| | |
+| :--------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | ESP32-DevKitC ([ESPRESSIF Official Store](https://fr.aliexpress.com/item/1005004441541467.html)) - Any version will work: 32 NodeMCU, 32s, 32e, 32ue, etc |
+| | ESP32-S3-DevKitC-1/1U ([ESPRESSIF Official Store](https://fr.aliexpress.com/item/1005003979778978.html)) |
+| | [LILYGO T-ETH-Lite ESP32-S3](https://www.lilygo.cc/products/t-eth-lite) (Ethernet) |
+| | [WT32-ETH01](https://fr.aliexpress.com/item/1005004436473683.html) v1.4 (Ethernet) |
+| | [WiFi Pigtail Antenna](https://fr.aliexpress.com/item/32957527411.html) for ESP32 boards supporting external WiFi antenna |
+
+### Robodyn
+
+| | |
+| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [Robodyn AC Dimmer 24A/600V](https://www.aliexpress.com/item/1005001965951718.html) Includes ZCD, supports **Phase Control** and **Burst mode** |
+| | [Robodyn AC Dimmer 40A/800V](https://fr.aliexpress.com/item/1005006211999051.html) Includes ZCD, supports **Phase Control** and **Burst mode** |
+| | Triac BTA40-800B RD91 [here](https://fr.aliexpress.com/item/32892486601.html) or [here](https://fr.aliexpress.com/item/1005001762265497.html) if you want / need to replace the Triac inside your Robodyn |
+| | [Heat Sink for Random SSR and Triac](https://fr.aliexpress.com/item/1005004879389236.html) (there are many more types available: take a big heat sink placed vertically) |
+
+> ##### IMPORTANT
+>
+> 1. It is possible to switch the TRIAC of an original Robodyn AC Dimmer with a higher one, for example a [BTA40-800B BOITIER RD-91](https://fr.farnell.com/stmicroelectronics/bta40-800b/triac-40a-800v-boitier-rd-91/dp/9801731)
+> Ref: [Triacs gradateurs pour routeur photovoltaĂŻque](https://f1atb.fr/fr/triac-gradateur-pour-routeur-photovoltaique/).
+>
+> 2. The heat sink must be chosen according to the SSR / Triac. Here is a good video about the theory: [Calcul du dissipateur pour le triac d'un routeur](https://www.youtube.com/watch?v=_zAx1Q2IvJ8) (from Pierre)
+>
+> 3. Make sure to [improve the Robodyn wiring/soldering](https://sites.google.com/view/le-professolaire/routeur-professolaire)
+> {: .block-important }
+
+### Random and Zero-Cross SSR
+
+| **Random and Zero-Cross SSR** | |
+| :---------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [LCTC Random Solid State Relay (SSR) that can be controlled by a 3.3V DC signal](https://www.aliexpress.com/item/1005004084038828.html), ([Other LCTC vendor link](https://fr.aliexpress.com/item/1005004863817921.html)). Supports **Phase Control** and **Burst mode**, See [How to choose your SSR ?](#how-to-choose-your-ssr-) below |
+| | [Zero-Cross Solid State Relay (SSR) that can be controlled by a 3.3V DC signal](https://fr.aliexpress.com/item/1005003216482476.html) Supports **Burst mode**, See [How to choose your SSR ?](#how-to-choose-your-ssr-) below |
+| | [Heat Sink for SSR](https://fr.aliexpress.com/item/32739226601.html) (there are many more types available: take a big heat sink placed vertically) |
+
+| **Zero-Cross Detection** | ZCD module is required with SSR. |
+| :------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| | [Very good ZCD module for DIN Rail from Daniel S.](https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html) (used in conjunction with a Random SSR) |
+
+**Other SSR:**
+
+- [Zero-Cross SSR DA](https://fr.aliexpress.com/item/1005002297502716.html)
+- [Zero-Cross SSR DA + Heat Sink + Din Rail Clip](https://www.aliexpress.com/item/1005002503185415.html) (40A, 60A, very high - can prevent closing an electric box)
+- [Zero-Cross SSR 120 DA](https://www.aliexpress.com/item/1005005020709764.html) (for very high load)
+
+### Voltage Regulators
+
+| | |
+| :----------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [Loncont LSA-H3P50YB](https://fr.aliexpress.com/item/32606780994.html) (also available in 70A and more). Includes ZCD, supports **Phase Control** and **Burst mode** |
+| | [LCTC Voltage Regulator 220V / 40A](https://fr.aliexpress.com/item/1005005008018888.html) or [more models without heat sink but 60A, 80A, etc](https://fr.aliexpress.com/item/20000003997748.html) (also available in 70A and more). Includes ZCD, supports **Phase Control** and **Burst mode** |
+| | [3.3V PWM Signal to 0-10V Convertor ](https://fr.aliexpress.com/item/1005004859012736.html) or [this link](https://fr.aliexpress.com/item/1005006859312414.html) or [this link](https://fr.aliexpress.com/item/1005007211285500.html) or [this link](https://fr.aliexpress.com/item/1005006822631244.html). Required to use the voltage regulators to convert the ESP32 pulse signal on 3.3V to an analogic output from 0-10V (external 12V power supply required) |
+| | [Heat Sink 10-60A for Voltage Regulators](https://fr.aliexpress.com/item/1005001541419957.html) |
+
+### Electromagnetic Relay
+
+| | |
+| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | 1-Channel 5V DC / 30A Electromagnetic Relay on DIN Rail Support: [here](https://fr.aliexpress.com/item/1005004908430389.html), [here](https://fr.aliexpress.com/item/32999654399.html), [here](https://fr.aliexpress.com/item/1005005870389973.html), [here](https://fr.aliexpress.com/item/1005005883440249.html) |
+| | 2-Channel 5V DC / 30A Dual Electromagnetic Relays on DIN Rail Support: [here](https://fr.aliexpress.com/item/1005004899369193.html), [here](https://fr.aliexpress.com/item/32999654399.html), [here](https://fr.aliexpress.com/item/1005005870389973.html), [here](https://fr.aliexpress.com/item/1005005883440249.html), [here](https://fr.aliexpress.com/item/1005001543232221.html) |
+
+### Measurement Devices
+
+| | |
+| :----------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [JSY-MK-194T with 1 fixed tore and 1 remote clamp](https://www.aliexpress.com/item/1005005396796284.html) Used to measure the grid power and total routed power |
+| | [JSY-MK-194T with 2 remote clamps](https://fr.aliexpress.com/item/1005005529999366.html) Used to measure the grid power and total routed power |
+| | Peacefair PZEM-004T V3 100A Openable (with clamp) [official](https://fr.aliexpress.com/item/33043137964.html), [with connector](https://fr.aliexpress.com/item/1005005984795952.html), [USB-TTL Cable](https://fr.aliexpress.com/item/1005006255175075.html). Can be used to measure each output individually and more precisely. Several PZEM-004T can be connected to the same Serial port. |
+| | [Shelly EM](https://www.shelly.com/en-fr/products/product-overview/shelly-em-120a/shelly-em-2x-50a) (or any other alternative sending data to MQTT) |
+
+### Temperature Sensors, LEDs, Buttons, Displays
+
+| **Temperature Sensors, LEDs, Buttons** | |
+| :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [DS18B20 Temperature Sensor + Adapter](https://fr.aliexpress.com/item/4000143479592.html) (easier to use to install in the water tank - take a long cable) |
+| | Push Buttons [Amazon](https://www.amazon.fr/dp/B0C2Y46BK6) (16mm) [AliExpress](https://fr.aliexpress.com/item/4001081212556.html) (12mm) for restart, manual bypass, reset |
+| | Traffic lights Lights module for system status [AZ-Delivery](https://www.az-delivery.de/en/products/led-ampel-modul), [AliExpress](https://fr.aliexpress.com/item/32957515484.html) |
+
+| **Screens** | |
+| :----------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
+| | [SSD1306 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/32638662748.html) |
+| | [SH1106 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/1005001621782442.html) |
+| | [SSD1307 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/1005003297480376.html) |
+
+### Mounting Accessories
+
+| | |
+| :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| | [Electric Box](https://www.amazon.fr/gp/product/B0BWFGVV4S) |
+| | [Extension boards](https://www.amazon.fr/dp/B0BCWBW4SR) (pay attention to the distance between header, there are different models. This one fits the ESP32 NodeMCU above) |
+| | [DIN Rail Mount for PCB 72mm x 20mm](https://fr.aliexpress.com/item/32276247838.html) for the ZCD module above to mount on DIN Rail. [Alternative link](https://fr.aliexpress.com/item/4000272944733.html) |
+| | [DIN Rail Mount for ESP32 NodeMCU Dev Kit C](https://fr.aliexpress.com/item/1005005096107275.html) |
+| | [Distribution Module](https://fr.aliexpress.com/item/1005005996836930.html) / [More choice](https://fr.aliexpress.com/item/1005006039723013.html) |
+| | [DIN Rail Clips for SSR](https://fr.aliexpress.com/item/1005004396715182.html) |
+| | AC-DC 5V 2.4A DIN Adapter HDR-15-5 [Amazon](https://www.amazon.fr/Alimentation-rail-Mean-Well-HDR-15-5/dp/B06XWQSJGW), [AliExpress](https://fr.aliexpress.com/item/4000513120668.html). Can be used to power the ESP when installed in an electric box on DIN rail. Also if you need, a 12V version s available: [HDR-15-15 12V DC version](https://www.amazon.fr/Alimentation-rail-Mean-Well-HDR-15-5/dp/B07942GFTH?th=1) |
+| | [3D Print enclosure for JSY-MK-194T](https://www.thingiverse.com/thing:6003867). You can screw it on an SSR DIN Rail to place your JSY on a DIN Rail in this enclosure. |
+| | [Dupont Cable Kit](https://fr.aliexpress.com/item/1699285992.html) |
+| | [100 ohms 0.1uF RC Snubber](https://www.quintium.fr/shelly/168-shelly-rc-snubber.html) (for Robodyn AC dimmer and Random SSR: can be placed at dimmer output) |
+
+## Default GPIO pinout per board
+
+The hardware and GPIO pinout are heavily inspired by [Routeur solaire PV monophasé Le Profes'Solaire](https://sites.google.com/view/le-professolaire/routeur-professolaire) from Anthony.
+Please read all the information there first.
+He did a very great job with some videos explaining the wiring.
+
+Most of the features can be enabled or disabled through the app and the GPIO pinout can be changed also trough the app.
+
+Here are below the default GPIO pinout for each board.
+
+**Tested boards:**
+
+| **FEATURE** | **ESP32** | **NodeMCU-32S** | **esp32s3** | **wt32_eth01** | **T-ETH-Lite** |
+| :-------------------------------- | :-------: | :-------------: | :---------: | :------------: | :------------: |
+| Display CLOCK (CLK) | 22 | 22 | 9 | 32 | 40 |
+| Display DATA (SDA) | 21 | 21 | 8 | 33 | 41 |
+| JSY-MK-194T RX (Serial TX) | 17 | 17 | 17 | 17 | 17 |
+| JSY-MK-194T TX (Serial RX) | 16 | 16 | 16 | 5 | 18 |
+| Light Feedback (Green) | 0 | 0 | 0 | -1 | 38 |
+| Light Feedback (Red) | 15 | 15 | 15 | -1 | 46 |
+| Light Feedback (Yellow) | 2 | 2 | 2 | -1 | 21 |
+| OUTPUT #1 Bypass Relay | 32 | 32 | 40 | 12 | 20 |
+| OUTPUT #1 Dimmer (Robodyn or SSR) | 25 | 25 | 37 | 2 | 19 |
+| OUTPUT #1 Temperature Sensor | 18 | 18 | 18 | 15 | 3 |
+| OUTPUT #2 Bypass Relay | 33 | 33 | 33 | -1 | 15 |
+| OUTPUT #2 Dimmer (Robodyn or SSR) | 26 | 26 | 36 | -1 | 7 |
+| OUTPUT #2 Temperature Sensor | 5 | 5 | 5 | -1 | 16 |
+| Push Button (restart) | EN | EN | EN | EN | EN |
+| RELAY #1 | 13 | 13 | 13 | 14 | 5 |
+| RELAY #2 | 12 | 12 | 12 | -1 | 6 |
+| System Temperature Sensor | 4 | 4 | 4 | 4 | 4 |
+| ZCD (Robodyn or ZCD Sync) | 35 | 35 | 35 | 35 | 8 |
+| PZEM-004T v3 RX (Serial TX) | 27 | 27 | 11 | -1 | -1 |
+| PZEM-004T v3 TX (Serial RX) | 14 | 14 | 14 | -1 | -1 |
+
+- `-1` means not mapped (probably because the board does not have enough pins)
+
+**Potential compatible boards, but not tested yet:**
+
+| **FEATURE** | **esp32-poe** | **ESP32-C3-DevKitC-02** | **lolin32_lite** | **lolin_s2_mini** | **m5stack-atom** | **m5stack-atoms3** |
+| :-------------------------------- | :-----------: | :---------------------: | :--------------: | :---------------: | :--------------: | :----------------: |
+| Display CLOCK (CLK) | 16 | 6 | 22 | 9 | -1 | -1 |
+| Display DATA (SDA) | 13 | 7 | 19 | 8 | -1 | -1 |
+| JSY-MK-194T RX (Serial TX) | 33 | 20 | 17 | 39 | -1 | -1 |
+| JSY-MK-194T TX (Serial RX) | 35 | 21 | 16 | 37 | -1 | -1 |
+| Light Feedback (Green) | -1 | -1 | 0 | 3 | -1 | -1 |
+| Light Feedback (Red) | -1 | -1 | 15 | 6 | -1 | -1 |
+| Light Feedback (Yellow) | -1 | -1 | 2 | 2 | -1 | -1 |
+| OUTPUT #1 Bypass Relay | 4 | 2 | 32 | 21 | -1 | -1 |
+| OUTPUT #1 Dimmer (Robodyn or SSR) | 2 | 1 | 25 | 10 | -1 | -1 |
+| OUTPUT #1 Temperature Sensor | 5 | 0 | 18 | 18 | -1 | -1 |
+| OUTPUT #2 Bypass Relay | -1 | 9 | 33 | 33 | -1 | -1 |
+| OUTPUT #2 Dimmer (Robodyn or SSR) | -1 | 8 | 26 | 11 | -1 | -1 |
+| OUTPUT #2 Temperature Sensor | -1 | 5 | 5 | 5 | -1 | -1 |
+| Push Button (restart) | EN | EN | EN | EN | EN | EN |
+| RELAY #1 | 14 | -1 | 13 | 13 | -1 | -1 |
+| RELAY #2 | 15 | -1 | 12 | 12 | -1 | -1 |
+| System Temperature Sensor | 0 | 4 | 4 | 4 | -1 | -1 |
+| ZCD (Robodyn or ZCD Sync) | 36 | 3 | 35 | 35 | -1 | -1 |
+| PZEM-004T v3 RX (Serial TX) | -1 | -1 | -1 | -1 | -1 | -1 |
+| PZEM-004T v3 TX (Serial RX) | -1 | -1 | -1 | -1 | -1 | -1 |
+
+- `-1` means not mapped (probably because the board does not have enough pins)
+- This mapping table might contain errors
+
+**Minimal requirements:**
+
+- a pin configured to the ZCD system: either the ZC pin of the Robodyn or any pin from any other ZC detection module
+- a pin configured to the Phase Control system: PSM pin for the Robodyn or DC + side of the Random SSR
+
+The website display the pinout configured, the pinout layout that is live at runtime and also displays some potential issues like duplicate pins or wrong pin configuration.
+
+[![](./assets/img/screenshots/gpio.jpeg)](./assets/img/screenshots/gpio.jpeg)
+
+## Pictures of some routers
+
+> _TO BE COMPLETED_
diff --git a/docs/buy.md b/docs/buy.md
new file mode 100644
index 0000000..dde5157
--- /dev/null
+++ b/docs/buy.md
@@ -0,0 +1,57 @@
+---
+layout: default
+title: Buy
+description: Buy
+---
+
+# Buy YaSolR Pro
+
+OSS and Pro firmware are the same, except that the PRO version relies on commercial (paid) libraries and provides some additional features based on a better dashboard.
+
+**The Pro version is only 25 euros** and gives access to all the perks of the Pro version below:
+
+| Feature | OSS (Free) | PRO (Paid) |
+| -------------------------------- | :--------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| Dashboard | Overview **only** | Full Dashboard as seen in the screenshots |
+| Manual Dimmer Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Manual Bypass Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Manual Relay Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Configuration | Debug Config Page | **From Dashboard**
Debug Config Page |
+| Automatic Resistance Calibration | ❌ | ✅ |
+| Energy Reset | ❌ | ✅ |
+| GPIO Config and Health | ❌ | ✅ |
+| Hardware Config and Health | ❌ | ✅ |
+| Output Statistics | ❌ | ✅ |
+| PID Tuning View | ❌ | ✅ |
+| PZEM Pairing | ❌ | ✅ |
+| Help & Support | [Facebook Group](https://www.facebook.com/groups/yasolr) | [Facebook Group](https://www.facebook.com/groups/yasolr)
[Forum](https://github.com/mathieucarbou/YaSolR-OSS/discussions)
[Bug Report](https://github.com/mathieucarbou/YaSolR-OSS/issues) |
+| Web Console | [WebSerial Lite](https://github.com/mathieucarbou/WebSerialLite) | [WebSerial Pro](https://www.webserial.pro) |
+| Dashboard | [ESP-DASH](https://github.com/ayushsharma82/ESP-DASH) | [ESP-DASH Pro](https://espdash.pro) |
+| OTA Firmware Update | [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) | [ElegantOTA Pro](https://elegantota.pro) |
+
+The money helps funding the hardware necessary to test and develop the firmware.
+
+## How to buy:
+
+1. Get a **[Github](https://github.com/)** account so that I can add your GitHub username to the project repository from where you can download all the firmware files.
+
+2. Make a donation of **25 euros or more** (through [Github](https://github.com/sponsors/mathieucarbou) or [Paypal](https://www.paypal.com/donate/?hosted_button_id=QJYRAPXGEDCNS)).
+ Any sponsoring of 25 euros or more will give access to the **Pro version and all the upcoming updates for an unlimited time**!
+
+> ##### IMPORTANT
+>
+> - Github is the preferred way to sponsor
+> - If you prefer Paypal, do not forget to add your GitHub username in the Paypal form (there will be a comment / note field for that).
+{: .block-important }
+
+Thanks a lot!
+
+# Sponsoring
+
+Any sponsoring is greatly appreciated to help me continue working in this project and all other project I maintain (see [all the Open-Source projects and Arduino / ESP32 libraries I have created](https://oss.carbou.me)).
+
+Here are 2 ways to sponsor:
+
+| **[Using GitHub](https://github.com/sponsors/mathieucarbou)
(Preferred way)** | **[Using Paypal](https://www.paypal.com/donate/?hosted_button_id=QJYRAPXGEDCNS)** |
+| :--------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: |
+| [![](assets/img/Github_Donate.png)](assets/img/Github_Donate.png) | [![](assets/img/Paypal_Donate.png)](assets/img/Paypal_Donate.png) |
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin
new file mode 100644
index 0000000..ec041cd
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.OTA.bin
new file mode 100644
index 0000000..d471cc2
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin
new file mode 100644
index 0000000..dea2168
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.OTA.bin
new file mode 100644
index 0000000..5d5c746
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin
new file mode 100644
index 0000000..c2e2e14
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.OTA.bin
new file mode 100644
index 0000000..90fa888
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin
new file mode 100644
index 0000000..ae4ec39
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.OTA.bin
new file mode 100644
index 0000000..c5da8be
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin
new file mode 100644
index 0000000..02de8ff
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.OTA.bin
new file mode 100644
index 0000000..eee1132
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin
new file mode 100644
index 0000000..97a981e
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.OTA.bin
new file mode 100644
index 0000000..df11a3e
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin
new file mode 100644
index 0000000..de8286f
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.OTA.bin
new file mode 100644
index 0000000..195bc4d
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin
new file mode 100644
index 0000000..c467981
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.OTA.bin
new file mode 100644
index 0000000..0851569
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin
new file mode 100644
index 0000000..7581be4
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.OTA.bin
new file mode 100644
index 0000000..724ba71
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.OTA.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin
new file mode 100644
index 0000000..9bd1e37
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin differ
diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.OTA.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.OTA.bin
new file mode 100644
index 0000000..06b087f
Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.OTA.bin differ
diff --git a/docs/download.md b/docs/download.md
new file mode 100644
index 0000000..0563a4e
--- /dev/null
+++ b/docs/download.md
@@ -0,0 +1,59 @@
+---
+layout: default
+title: Download
+description: Download
+---
+
+# YaSolR Downloads
+
+**Please make sure to download the firmware matching your board.**
+
+Firmware files are named as follow:
+
+- `YaSolR----.OTA.bin`: This firmware is used to update through the Web OTA interface
+- `YaSolR----.FACTORY.bin`: This firmware is used for a first ESP installation, or wen doing a factory reset through USB flashing
+
+Where:
+
+- `VERSION`: YaSolR version, or `main` for the latest development build
+- `MODEL`: `oss`, `pro`
+- `BOARD`: the board type
+- `LANG`: `en`, `fr`, ...
+
+## Open-Source versions
+
+- [latest](https://github.com/mathieucarbou/YaSolR-OSS/releases/tag/latest) (latest development build can be unstable)
+
+_Firmware and source code for the Open-Source version are available directly in the GitHub project at [https://github.com/mathieucarbou/YaSolR-OSS/releases](https://github.com/mathieucarbou/YaSolR-OSS/releases)._
+
+## Pro versions
+
+- [latest](https://github.com/mathieucarbou/YaSolR-Pro/tree/main/latest) (latest development build can be unstable)
+
+_Firmware for the Pro version are only available to Pro users. You must be logged into your GitHub account to access them._
+
+Please go to the [Buy](buy) page if you are interested in buying the Pro version.
+
+### Trial versions
+
+_Version 1.0.0 coming soon..._
+
+- [YaSolR-1.0.0-trial-esp32-en.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32-en.FACTORY.bin)
+- [YaSolR-1.0.0-trial-esp32-en.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32-en.OTA.bin)
+- [YaSolR-1.0.0-trial-esp32-fr.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32-fr.FACTORY.bin)
+- [YaSolR-1.0.0-trial-esp32-fr.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32-fr.OTA.bin)
+- [YaSolR-1.0.0-trial-esp32s3-en.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32s3-en.FACTORY.bin)
+- [YaSolR-1.0.0-trial-esp32s3-en.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32s3-en.OTA.bin)
+- [YaSolR-1.0.0-trial-esp32s3-fr.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32s3-fr.FACTORY.bin)
+- [YaSolR-1.0.0-trial-esp32s3-fr.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-esp32s3-fr.OTA.bin)
+- [YaSolR-1.0.0-trial-lilygo_eth_lite_s3-en.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-lilygo_eth_lite_s3-en.FACTORY.bin)
+- [YaSolR-1.0.0-trial-lilygo_eth_lite_s3-en.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-lilygo_eth_lite_s3-en.OTA.bin)
+- [YaSolR-1.0.0-trial-lilygo_eth_lite_s3-fr.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-lilygo_eth_lite_s3-fr.FACTORY.bin)
+- [YaSolR-1.0.0-trial-lilygo_eth_lite_s3-fr.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-lilygo_eth_lite_s3-fr.OTA.bin)
+- [YaSolR-1.0.0-trial-wt32_eth01-en.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-wt32_eth01-en.FACTORY.bin)
+- [YaSolR-1.0.0-trial-wt32_eth01-en.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-wt32_eth01-en.OTA.bin)
+- [YaSolR-1.0.0-trial-wt32_eth01-fr.FACTORY.bin](/downloads/trials/YaSolR-1.0.0-trial-wt32_eth01-fr.FACTORY.bin)
+- [YaSolR-1.0.0-trial-wt32_eth01-fr.OTA.bin](/downloads/trials/YaSolR-1.0.0-trial-wt32_eth01-fr.OTA.bin)
+
+_Trial versions are like the Pro version but will stop after **3 days of uptime**._
+_If you need to extend your trial period: you can re-flash the factory firmware after erasing the flash, put your settings back, and you will be good for another 3 days of trial._
diff --git a/docs/downloads/auto_diverter_v1.js b/docs/downloads/auto_diverter_v1.js
new file mode 100644
index 0000000..4c682a4
--- /dev/null
+++ b/docs/downloads/auto_diverter_v1.js
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (C) 2023-2024 Mathieu Carbou
+ *
+ * ======================================
+ * CHANGELOG
+ *
+ * - v1: Initial version
+ *
+ * ======================================
+ */
+const scriptName = "auto_diverter";
+
+// Config
+
+const CONFIG = {
+ // Debug mode
+ DEBUG: 1,
+ // Grid Power Read Interval (s)
+ READ_INTERVAL_S: 1,
+ PID: {
+ // Reverse
+ REVERSE: false,
+ // Proportional Mode:
+ // - "error" (proportional on error),
+ // - "input" (proportional on measurement),
+ // - "both" (proportional on 50% error and 50% measurement)
+ P_MODE: "input",
+ // Derivative Mode:
+ // - "error" (derivative on error),
+ // - "input" (derivative on measurement)
+ D_MODE: "error",
+ // Integral Correction
+ // - "off" (integral sum not clamped),
+ // - "clamp" (integral sum not clamped to OUT_MIN and OUT_MAX),
+ // - "advanced" (advanced anti-windup algorithm)
+ IC_MODE: "advanced",
+ // Target Grid Power (W)
+ SETPOINT: 0,
+ // PID Proportional Gain
+ KP: 0.3,
+ // PID Integral Gain
+ KI: 0.3,
+ // PID Derivative Gain
+ KD: 0.1,
+ // Output Minimum (W)
+ OUT_MIN: -10000,
+ // Output Maximum (W)
+ OUT_MAX: 10000,
+ },
+ DIMMERS: {
+ "192.168.125.98": {
+ // Resistance (in Ohm) of the load connecter to the dimmer + voltage regulator
+ // 0 will disable the dimmer
+ RESISTANCE: 24,
+ // Percentage of the remaining excess power that will be assigned to this dimmer
+ // The remaining percentage will be given to the next dimmers
+ RESERVED_EXCESS_PERCENT: 100,
+ // Set whether the Shelly EM with this script will be used to control the bypass relay to force a heating
+ // When set to true, if you activate remotely the bypass to force a heating, then the script will detect it and turn the dimmer off
+ BYPASS_CONTROLLED_BY_EM: true
+ },
+ "192.168.125.97": {
+ RESISTANCE: 0,
+ RESERVED_EXCESS_PERCENT: 100,
+ BYPASS_CONTROLLED_BY_EM: false
+ }
+ }
+};
+
+// PID Controller
+
+let PID = {
+ // PID Input
+ input: 0,
+ // PID Output
+ output: 0,
+ // current error value
+ error: 0,
+ // Proportional Term
+ pTerm: 0,
+ // Integral Term
+ iTerm: 0,
+ // Derivative Term
+ dTerm: 0,
+ // Sum
+ sum: 0,
+};
+
+// Divert Control
+
+let DIVERT = {
+ lastTime: 0,
+ dimmers: {}
+};
+
+function validateConfig(cb) {
+ print(scriptName, ":", "Validating Config...");
+
+ if (CONFIG.DIMMERS.length === 0) {
+ print(scriptName, ":", "ERR: No dimmer configured");
+ return;
+ }
+
+ for (let ip in CONFIG.DIMMERS) {
+ if (CONFIG.DIMMERS[ip].RESISTANCE < 0) {
+ print(scriptName, ":", "ERR: Dimmer resistance should be greater than 0");
+ return;
+ }
+
+ if (CONFIG.DIMMERS[ip].RESISTANCE === 0) {
+ print(scriptName, ":", "Dimmer", ip, "is disabled");
+ continue;
+ }
+
+ print(scriptName, ":", "Dimmer", ip, "is enabled");
+ DIVERT.dimmers[ip] = {
+ divertPower: 0
+ };
+ }
+
+ cb();
+}
+
+function constrain(value, min, max) { return Math.min(Math.max(value, min), max); }
+
+// - https://github.com/Dlloydev/QuickPID
+// - https://github.com/br3ttb/Arduino-PID-Library
+function calculatePID(input) {
+ const dInput = CONFIG.PID.REVERSE ? PID.input - input : input - PID.input;
+ const error = CONFIG.PID.REVERSE ? input - CONFIG.PID.SETPOINT : CONFIG.PID.SETPOINT - input;
+ const dError = error - PID.error;
+
+ if (CONFIG.DEBUG > 1) {
+ print(scriptName, ":", "Input:", input, "W, Error:", error, "W, dError:", dError, "W");
+ }
+
+ let peTerm = CONFIG.PID.KP * error;
+ let pmTerm = CONFIG.PID.KP * dInput;
+ switch (CONFIG.PID.P_MODE) {
+ case "error":
+ pmTerm = 0;
+ break;
+ case "input":
+ peTerm = 0;
+ break;
+ case "both":
+ peTerm *= 0.5;
+ pmTerm *= 0.5;
+ break;
+ default:
+ return PID.output;
+ }
+
+ // pTerm
+ PID.pTerm = peTerm - pmTerm;
+
+ // iTerm
+ PID.iTerm = CONFIG.PID.KI * error;
+
+ if (CONFIG.DEBUG > 1) {
+ print(scriptName, ":", "pTerm:", PID.pTerm, "W, iTerm:", PID.iTerm, "W");
+ }
+
+ // anti-windup
+ if (CONFIG.PID.IC_MODE == "advanced" && CONFIG.PID.KI) {
+ const iTermOut = PID.pTerm + CONFIG.PID.KI * (PID.iTerm + error);
+ if ((iTermOut > CONFIG.PID.OUT_MAX && dError > 0) || (iTermOut < CONFIG.PID.OUT_MIN && dError < 0)) {
+ _iTerm = constrain(iTermOut, -CONFIG.PID.OUT_MAX, CONFIG.PID.OUT_MAX);
+ }
+ }
+
+ // integral sum
+ PID.sum = CONFIG.PID.IC_MODE == "off" ? (PID.sum + PID.iTerm - pmTerm) : constrain(PID.sum + PID.iTerm - pmTerm, CONFIG.PID.OUT_MIN, CONFIG.PID.OUT_MAX);
+
+ // dTerm
+ switch (CONFIG.PID.D_MODE) {
+ case "error":
+ PID.dTerm = CONFIG.PID.KD * dError;
+ break;
+ case "input":
+ PID.dTerm = -CONFIG.PID.KD * dInput;
+ break;
+ default:
+ return PID.output;
+ }
+
+ PID.output = constrain(PID.sum + peTerm + PID.dTerm, CONFIG.PID.OUT_MIN, CONFIG.PID.OUT_MAX);
+
+ PID.input = input;
+ PID.error = error;
+
+ return PID.output;
+}
+
+function callDimmerCallback(result, errCode, errMessage, data) {
+ if (errCode) {
+ print(scriptName, ":", "ERR callDimmerCallback:", errCode);
+ data.dimmer.rpc = "failed";
+ } else if (result.code !== 200) {
+ const rpcResult = JSON.parse(result.body);
+ print(scriptName, ":", "ERR", rpcResult.code, ":", rpcResult.message);
+ data.dimmer.rpc = "failed";
+ } else {
+ data.dimmer.rpc = "success";
+ }
+ data.cb();
+}
+
+function callDimmer(ip, dimmer, cb) {
+ const url = "http://" + ip + "/rpc/Light.Set?id=0&on=" + (dimmer.dutyCycle > 0 ? "true" : "false") + "&brightness=" + (dimmer.dutyCycle * 100) + "&transition_duration=0.5";
+ if (CONFIG.DEBUG > 1)
+ print(scriptName, ":", "Calling Dimmer: ", url);
+ Shelly.call("HTTP.GET", { url: url, timeout: 5 }, callDimmerCallback, { dimmer: dimmer, cb: cb });
+}
+
+function callDimmers(cb) {
+ function recallMe() {
+ callDimmers(cb)
+ }
+
+ for (let ip in DIVERT.dimmers) {
+ const dimmer = DIVERT.dimmers[ip];
+
+ // ignore contacted dimmers
+ if (dimmer.rpc !== "pending") {
+ continue;
+ }
+
+ if (isNaN(dimmer.dutyCycle)) {
+ dimmer.rpc = "failed";
+ print(scriptName, ":", "ERR: Invalid duty cycle for dimmer", ip);
+ continue;
+ }
+
+ // call dimmer
+ callDimmer(ip, dimmer, recallMe);
+
+ // exit the loop immediately to avoid multiple calls in case the yare made in parallel
+ return;
+ }
+
+ // if we are here, all dimmers have been contacted
+ cb();
+}
+
+function onSwitchGetStatus(result, errCode, errMessage, data) {
+ if (errCode) {
+ print(scriptName, ":", "ERR onSwitchGetStatus:", errCode);
+ throttleReadPower();
+ return;
+ }
+ if (CONFIG.DEBUG > 1)
+ print(scriptName, ":", "onSwitchGetStatus:", JSON.stringify(result));
+ if (result.output) {
+ for (let ip in DIVERT.dimmers) {
+ const dimmer = DIVERT.dimmers[ip];
+ if (CONFIG.DIMMERS[ip].BYPASS_CONTROLLED_BY_EM) {
+ print(scriptName, ":", "Bypass is ON, turning off dimmer", ip);
+ dimmer.apparentPower = 0;
+ dimmer.current = 0;
+ dimmer.dimmedVoltage = 0;
+ dimmer.divertPower = 0;
+ dimmer.dutyCycle = 0;
+ dimmer.powerFactor = 0;
+ dimmer.thdi = 0;
+ }
+ }
+ }
+ callDimmers(throttleReadPower);
+}
+
+function divert(voltage, gridPower) {
+ let newRoutingPower = calculatePID(gridPower);
+
+ if (CONFIG.DEBUG > 0)
+ print(scriptName, ":", "Grid:", voltage, "V,", gridPower, "W => To divert:", newRoutingPower, "W");
+
+ for (let ip in DIVERT.dimmers) {
+ const dimmer = DIVERT.dimmers[ip];
+ dimmer.maximumPower = voltage * voltage / CONFIG.DIMMERS[ip].RESISTANCE;
+ dimmer.divertPower = Math.min(newRoutingPower * CONFIG.DIMMERS[ip].RESERVED_EXCESS_PERCENT / 100, dimmer.maximumPower);
+ dimmer.dutyCycle = dimmer.divertPower / dimmer.maximumPower;
+ dimmer.powerFactor = Math.sqrt(dimmer.dutyCycle);
+ dimmer.dimmedVoltage = dimmer.powerFactor * voltage;
+ dimmer.current = dimmer.dimmedVoltage / CONFIG.DIMMERS[ip].RESISTANCE;
+ dimmer.apparentPower = dimmer.current * voltage;
+ dimmer.thdi = dimmer.dutyCycle === 0 ? 0 : Math.sqrt(1 / dimmer.dutyCycle - 1);
+ dimmer.rpc = "pending";
+
+ newRoutingPower -= dimmer.divertPower;
+
+ if (CONFIG.DEBUG > 0)
+ print(scriptName, ":", "Dimmer", ip, "=>", dimmer.divertPower, "W");
+ }
+
+ Shelly.call("Switch.GetStatus", { id: 0 }, onSwitchGetStatus);
+}
+
+function onEM1GetStatus(result, errCode, errMessage, data) {
+ if (errCode) {
+ print(scriptName, ":", "ERR onEM1GetStatus:", errCode);
+ throttleReadPower();
+ return;
+ }
+ if (CONFIG.DEBUG > 1)
+ print(scriptName, ":", "EM1.GetStatus:", JSON.stringify(result));
+ divert(result.voltage, result.act_power);
+}
+
+function readPower() {
+ DIVERT.lastTime = Date.now();
+ Shelly.call("EM1.GetStatus", { id: 0 }, onEM1GetStatus);
+}
+
+function throttleReadPower() {
+ const now = Date.now();
+ const diff = now - DIVERT.lastTime;
+ if (diff > 1000) {
+ readPower();
+ } else {
+ Timer.set(1000 - diff, false, readPower);
+ }
+}
+
+// HTTP handlers
+
+function onHttpGetStatus(request, response) {
+ response.code = 200;
+ response.headers = {
+ "Content-Type": "application/json"
+ };
+ response.body = JSON.stringify({
+ config: CONFIG,
+ pid: PID,
+ divert: DIVERT
+ });
+ response.send();
+}
+
+// Main
+
+validateConfig(function () {
+ print(scriptName, ":", "Starting Shelly Solar Diverter...");
+ HTTPServer.registerEndpoint("status", onHttpGetStatus);
+ readPower();
+});
diff --git a/docs/downloads/solar_diverter_en.md b/docs/downloads/solar_diverter_en.md
new file mode 100644
index 0000000..49fdeb3
--- /dev/null
+++ b/docs/downloads/solar_diverter_en.md
@@ -0,0 +1,41 @@
+The Shelly Solar Diverter/Router
+
+Hello,
+
+I wanted to share with you a small project that I carried out in the recent days as part of the "Shelly IoT Innovation Challenge" and which will surely pick your interest: a solar diverter/router that is quite innovative because it is very simple and based on... Shelly's of course!
+
+I always wanted to find a **simple** way to control the **Loncont LSA-H3P50YB** voltage regulator, which is interesting because in addition to being sturdy, it includes a Zero-Cross Detection module and is controlled very easily by voltage variation or PWM.
+In particular, it can be controlled by an ESP32 but using an external module and an additional 12V power supply, which is not very practical.
+
+A few days ago, a friend pointed out to me that Shelly has a 0-10V dimmer, the voltage range required to control this regulator, but for it to work, the dimmer must be of the "sourcing current" type and not "sinking current".
+
+Except... Shelly has just released a new dimmer very recently: the **Shelly Dimmer 0/1-10V PM Gen3**, which is precisely of a "sourcing current" type!
+
+What a joy!
+
+So I decided to write a Shelly script which allows you to automatically control this type of voltage regulator via the Shelly Dimmer 0/1-10V PM Gen3, depending on the injection or consumption read from the Shelly EM Pro (purchased from Quintium), which also reads the output power of the regulator with its 2nd current clamp.
+
+I have not yet been able to test in the long term, but the small tests carried out for the moment show that it works, within the possible limit of the precision that can be obtained using the Shelly scripts: reading the measurements at each second and then a call to adjust the dimmer immediately afterwards.
+
+It is therefore a promising solution for the moment, and which remains easy to improve.
+
+Also, dimmers tend to get hot, so I suspect it needs to be installed not within an enclosure.
+
+Features and benefits of this diverter/router:
+
+- Shelly and LSA Loncont components are robust, compliant with standards, and used in industry
+- Very easy to set up (Shelly script)
+- Automatic divert management via a PID controller which supports several proportional and derivative modes
+- Supports a bypass contactor (to force a heating), which will automatically cut off the dimmers if turned on
+- Supports a DS1820 temperature probe via the Shelly Add-on to get the water tank temperature (or anything else)
+- Support for up to N dimmers, with possible sharing of the excess power between dimmers
+- And of course, everything you can have with remote control of Shelly's in the Shelly App, Home Assistant / Jeedom, etc.
+
+It's then up to you to write your Shelly automations to program forced operation, start or stop automatic routing remotely, etc.
+Full of possibilities with Shelly's!
+
+The script can be downloaded and modified as you wish (it is under the MIT license), and it can be found on the blog of the YaSolR site, the routing software I've been working for several months now:
+
+https://yasolr.carbou.me/blog/2024-07-01_shelly_solar_diverter
+
+Happy hacking!
diff --git a/docs/downloads/solar_diverter_fr.md b/docs/downloads/solar_diverter_fr.md
new file mode 100644
index 0000000..2631c19
--- /dev/null
+++ b/docs/downloads/solar_diverter_fr.md
@@ -0,0 +1,41 @@
+Le Routeur Solaire Shelly
+
+Bonjour la communauté!
+
+Je souhaitais vous partager un petit projet que j'ai réalisé ces derniers jours dans le cadre du _"Shelly IoT Innovation Challenge"_ et qui va sûrement vous intéresser: un routeur solaire assez innovant car très simple et basé sur... les Shelly!
+
+J'ai toujours souhaité trouvé un moyen **simple** de contrôler le régulateur de tension **Loncont LSA-H3P50YB**, qui est intéressant car en plus d'être costaud, il comprend un module Zero-Detection et se contrôle donc très facilement par variation de tension ou PWM.
+On peut notamment le contrôler par un ESP32 mais à l'aide d'un module externe et une alimentation de 12V supplémentaire, ce qui n'est pas très pratique.
+
+Il y a quelques jours, Mathieu Hertz m'a fait remarqué que Shelly ont des dimmers 0-10V, plage de tension justement requise au contrôle de ce régulateur, mais pour que cela fonctionne, il faut que le dimmer soit de type "sourcing current" et non "sinking current".
+
+Hors... Shelly vient justement de sortie un nouveau dimmer depuis très peu: le **Shelly Dimmer 0/1-10V PM Gen3**, qui est justement de type "sourcing current"!
+
+Quel bonheur!
+
+J'ai donc décidé d'écrire un **script Shelly** qui permet de contrôler automatiquement ce genre de régulateur de tension via le Shelly Dimmer 0/1-10V PM Gen3, en fonction de la l'injection ou consommation lue à partir du Shelly EM Pro (acheté chez Quintium), qui lit aussi la puissance en sortie du régulateur avec sa 2ème pince ampèremétrique.
+
+Je n'ai pas pu tester encore sur du long terme, mais les petits tests effectués pour le moment montrent que ça fonctionne, dans la limite possible de la précision qu'on peut avoir en passant par les scripts Shelly: lecture des mesure à chaque seconde et appel pour régler le dimmer de suite après.
+
+C'est donc une solution prometteuse pour le moment, et qui reste facile à améliorer.
+
+Également, les dimmer ont tendance à chauffer, donc je soupçonne qu'il faille l'installer espacé et dans un endroit aéré.
+
+Fonctionnalités et avantages de ce routeur:
+
+- Les composants Shelly et LSA Loncont, sont robustes, aux normes, et utilisés en industrie
+- Très facile à mettre en place (script Shelly)
+- Gestion automatique du routage via un contrôleur PID qui supporte plusieurs modes de proportionnelles et dérivées
+- Supporte un contacteur pour la Marche forcée, qui va automatiquement couper les dimmer si mis en marche
+- Supporte une sonde de température DS1820 via le Shelly Add-on pour avoir la température du ballon
+- Support jusqu'Ă N dimmers, avec un partage possible du surplus entre dimmers
+- Et bien sûr, tout ce qu'on peut avoir avec le contrôle à distance des Shelly via l'app Shelly, Home Assistant / Jeedom, etc.
+
+À vous ensuite d'écrite vos automatismes Shelly pour programmer la marche forcée, démarrer ou arrêter le routage automatique à distance, etc.
+Plein de possibilité avec Shelly!
+
+Le script peut être téléchargé et modifié à votre guise (il est sous license MIT), et il se trouve sur le blog du site YaSolR, le logiciel de routage sur lequel je travaille depuis quelques mois:
+
+https://yasolr.carbou.me/blog/2024-07-01_shelly_solar_diverter
+
+Bonne lecture!
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..422d7e5
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,274 @@
+---
+layout: default
+title: Home
+description: Home
+---
+
+# YaSolR ?
+
+YaSolR is an ESP32 firmware for **Solar Router** compatible with nearly **any existing hardware components**.
+
+If you already have a Solar Router at home based on ESP32, built by yourself or someone else, there is a good chance that YaSolR will be compatible.
+
+| [![](./assets/img/screenshots/overview.jpeg)](./assets/img/screenshots/overview.jpeg) | [![](./assets/img/screenshots/output1.jpeg)](./assets/img/screenshots/output1.jpeg) |
+
+YaSolR **is a software** that will run on your Solar Router.
+
+YaSolR **does not come with hardware**.
+But this website will help you pick and build your router.
+
+## Benefits
+
+YaSolR is one of the **most optimized and powerful** Solar Router firmware available:
+
+- **12-bits resolution** with linear interpolation for a precise TRIAC control
+- **25 measurements / second** with a local JSY
+- **20 measurements / second** with a remote JSY
+- **PID Controller** optimized and customizable
+- **PID Tuning** web interface
+- **RMT Peripheral** used for DS18 readings
+- **Harmonics** can be lowered to comply with regulations thanks to several settings
+- **Custom dimmer library** optimized for ESP32 (🚧)
+- **MCPWM** (Motor Control Pulse Width Modulator) for phase control (🚧)
+- **3-Phase** support
+
+This is a big **Open-Source** project following **best development practices**.
+YaSolR is:
+
+- **flexible** by allowing you to pick the hardware you want
+- **easy to use** with one of the **best easy-to-use and responsive Web Interface**, **REST** API and **MQTT** API
+- compatible with **Home Assistant** and other home automation systems (Auto Discovery)
+- compatible with **EV Charging Box** like OpenEVSE
+- compatible with **110/230V 50/60Hz**
+- compatible with **remote** dimmer, relay, temperature, measurement (**ESP-Now** / **UDP**)
+- compatible with **many hardware components**:
+ - ESP32 Dev Kit
+ - ESP32 S3 Dev Kit
+ - ESP32s
+ - Lilygo T Eth Lite S3 (**Ethernet support**)
+ - WT32-ETH01 (**Ethernet support**)
+ - Random and Zero-Cross SSR
+ - Standalone Zero-Cross Detection modules
+ - Robodyn 24A / 40A
+ - Voltage Regulators (Loncont LSA or LCTC)
+ - DS18 Temperature Sensors
+ - etc
+
+## Detailed Features
+
+- [Routing Outputs](#routing-outputs)
+ - [Dimmer (required)](#dimmer-required)
+ - [Bypass Relay (optional)](#bypass-relay-optional)
+ - [Temperature (optional)](#temperature-optional)
+ - [Measurement device (optional)](#measurement-device-optional)
+ - [Additional Features](#additional-output-features)
+- [Grid Power Measurement](#grid-power-measurement)
+ - [JSY-MK-194T](#jsy-mk-194t)
+ - [Remote Grid Measurement](#remote-grid-measurement)
+ - [3-Phase Support](#3-phase-support)
+- [2x Relays](#2x-relays)
+- [Monitoring and Management](#monitoring-and-management)
+- [MQTT, REST API and Home Automation Systems](#mqtt-rest-api-and-home-automation-systems)
+- [Online / Offline modes](#online--offline-modes)
+- [PID Control and Tuning](#pid-control-and-tuning)
+- [Remote Capabilities](#remote-capabilities)
+- [Virtual Excess and EV Charger Compatibility](#virtual-excess-and-ev-charger-compatibility)
+
+### Routing Outputs
+
+YaSolR supports up to 2 outputs.
+A routing output is connected to a resistive load and controls its power by dimming the voltage.
+Each output is composed of the following components:
+
+- [Dimmer (required)](#dimmer-required)
+- [Bypass Relay (optional)](#bypass-relay-optional)
+- [Temperature (optional)](#temperature-optional)
+- [Measurement device (optional)](#measurement-device-optional)
+- [Additional Output Features](#additional-output-features)
+
+#### Dimmer (required)
+
+A dimmer controls the power sent to the load.
+Example of supported dimmers:
+
+| Dimmer Type | `Phase Control` | `Burst Fire Control` (🚧) |
+| :------------------------------------------------------------------------- | :-------------: | :-----------------------: |
+| **Robodyn 24A**
![](./assets/img/hardware/Robodyn_24A.jpeg) | âś… | âś… |
+| **Robodyn 40A**
![](./assets/img/hardware/Robodyn_40A.jpeg) | âś… | âś… |
+| **Random SSR**
![](./assets/img/hardware/Random_SSR.jpeg) | âś… | âś… |
+| **Zero-Cross SSR** (🚧)
![](./assets/img/hardware/SSR_40A_DA.jpeg) | ❌ | ✅ |
+| **Voltage Regulator** (🚧)
![](./assets/img/hardware/LSA-H3P50YB.jpeg) | âś… | âś… |
+
+#### Bypass Relay (optional)
+
+A bypass relay allows to force a heating at full power and bypass the dimmer at a given schedule or manually.
+Keeping a dimmer `on` generates heat so a bypass relay can be installed to avoid using the dimmer.
+
+**If a bypass relay is installed, the dimmer will be used instead and will be set to 0-100% to simulate the relay.**
+
+| Electromagnetic Relay | Zero-Cross SSR | Random SSR |
+| :--------------------------------------------------------------: | :------------------------------------------------------: | :--------------------------------------------------: |
+| ![Electromagnetic Relay](./assets/img/hardware/DIN_2_Relay.jpeg) | ![Zero-Cross SSR](./assets/img/hardware/SSR_40A_DA.jpeg) | ![Random SSR](./assets/img/hardware/Random_SSR.jpeg) |
+
+#### Temperature (optional)
+
+Measuring the temperature of the water tanker is important to ve able to trigger automatic heating based on temperature thresholds, or stop the routing if the temperature i reached.
+
+This can also be done:
+
+- using a remote temperature sensor through MQTT
+- using a remote temperature sensor through ESP-Now (🚧)
+- using one of the supported sensor:
+
+| DS18B20 |
+| :--------------------------------------------: |
+| ![DS18B20](./assets/img/hardware/DS18B20.jpeg) |
+
+#### Measurement device (optional)
+
+Each output supports an optional measurement device to measure the power routed to the load.
+
+- `JSY-MK-194T`: has 2 clamps, and is used to monitor the aggregated routed output power (sum of the two outputs) and the grid power with the second clamp.
+- `PZEM-004T V3`: can monitor each output independently. **It cannot be used to measure the grid power.**
+
+| PZEM-004T V3 | JSY-MK-194T |
+| :---------------------------------------------------: | :------------------------------------------------------: |
+| ![PZEM-004T V3](./assets/img/hardware/PZEM-004T.jpeg) | ![JSY-MK-194T](./assets/img/hardware/JSY-MK-194T_2.jpeg) |
+
+#### Additional Output Features
+
+- `Bypass Automatic Control` / `Manual Control`: Automatically force a heating as needed based on days, hours, temperature range, or control it manually
+- `Dimmer Automatic Control` / `Manual Control`: Automatically send the grid excess to the resistive load through the dimmer (or manually control the dimmer yourself if disabled)
+- `Independent or Sequential Outputs with Grid Excess Sharing`: Outputs are sequential by default (second output activated after first one at 100%).
+ **Independent outputs are also supported** thanks to the sharing feature to split the excess between outputs.
+ This feature is available in `Dimmer Automatic Control` mode.
+- `Dimmer Duty Limiter`: Set a limit to the dimmer power to avoid routing too much power
+- `Dimmer Temperature Limiter`: Set a limit to the dimmer to stop it when a temperature is reached. This temperature can be different than the temperature used in auto bypass mode.
+- `Statistics`: Harmonic information, power factor, energy, routed power, etc
+- `Automatic Resistance Calibration` (🚧): if a JSY or PZEM is installed, automatically discover and save the resistance value of the connected load.
+
+### Grid Power Measurement
+
+Measuring the grid power is essential to know how much power is available to route.
+YaSolR supports many ways to measure the grid power and voltage:
+
+**Mono-phase**:
+
+- `MQTT` (**Home Assistant**, **Jeedom**, `Shelly EM`, etc)
+- `JSY-MK-194T` (25 measurements / second)
+- Remote `JSY-MK-194T` through **UDP** (20 measurements / second)
+- Remote `JSY-MK-194T` through **ESP-Now** (🚧)
+
+**3-Phase**:
+
+- `MQTT` (**Home Assistant**, **Jeedom**, `Shelly 3EM`, etc)
+- `JSY-MK-333` (🚧)
+- Remote `JSY-MK-333` through **UDP** (🚧)
+- Remote `JSY-MK-333` through **ESP-Now** (🚧)
+
+### Relays
+
+YaSolR supports up to 2 relays to control external resistive loads or contactors.
+
+- Supports `NO / NC` relay type
+- `Automatic Control` / `Manual Control`: You can specify the resistive load power in watts connected to the relay.
+ If you do so, the relay will be automatically activated based on the excess power.
+
+### Monitoring and Management
+
+**Dashboard**:
+
+- `Charts`: Live charts (grid power, routed power, THDi, PID tuning, etc)
+- `Health`: configuration mistakes are detected as much as possible and issues displayed when a component was found to not properly work.
+- `Languages (i18n)`: en / fr
+- `LEDs`: Support LEDs for visual alerts
+- `Manual Override`: Override anything remotely (MQTT, REST, Website)
+- `GPIO`: A GPIO section shows the view of configured pins and activated pins, to report issues, duplications or invalid pins.
+- `Restart`, `Factory Reset`, `Config Backup & Restore`, `Debug Logging`
+- `Statistics`: Harmonic information, power factor, energy, routed power, grid power, grid frequency and voltage, etc
+- `Web console`: View ESP logs live from a Web interface
+- `Web OTA Firmware Update`: Update firmware through the Web interface
+
+**Hardware**:
+
+- `Display`: Support I2C OLED Display (`SSD1307`, `SH1106`, `SH1107`)
+- `Push Button`: Support a push button to restart the device
+- `Temperature Sensor`: Support a temperature rensor for the router box (`DS18B20`)
+
+### MQTT, REST API and Home Automation Systems
+
+The router exposes a lot of statistics and information through MQTT and REST API and provides a very good integration with Home Assistant or other home automation systems.
+The router can be completely controlled remotely through a Home Automation System.
+
+- `REST API`: extensive REST API support
+- `MQTT`: extensive MQTT API (with `TLS` support)
+- [Home Assistant Integration](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery): Activate Home Assistant Auto Discovery to automatically create a YaSolR device in Home Assistant with all the sensors
+
+### Online / Offline modes
+
+- `Access Point Mode`: router can **work in AP mode without WiFi and Internet**
+- `Admin Password`: to protect the website, API and Access Point
+- `Captive Portal` a captive portal is started first time to help you connect the router
+- `DNS & mDNS`: Discover the device on the network through mDNS (Bonjour) or DNS
+- `Ethernet & Wifi`: **ESP32 boards with Ethernet and WiFi are supported**
+- `NTP` support to synchronize time and date with Internet. If not activated, it is still possible to manually sync with your browser.
+- `Offline Mode`: **The router can work without WiFi, even teh features requiring time and date.**
+
+### PID Control and Tuning
+
+The router uses a PID controller to control the dimmers and you have full control over the PID parameters to tune it.
+
+Demo on Youtube:
+
+[![PID Tuning in YaSolR (Yet Another Solar Router)](https://img.youtube.com/vi/ygSpUxKYlUE/0.jpg)](https://www.youtube.com/watch?v=ygSpUxKYlUE "PID Tuning in YaSolR (Yet Another Solar Router)")
+
+### Virtual Excess and EV Charger Compatibility
+
+Thanks to power measurement, the router also provides these features:
+
+- `Virtual Excess`: Expose the virtual excess (MQTT, REST API, web) which is composed of the current excess plus the routing power
+- `EV Charger Compatibility` (i.e OpenEVSE): Don't prevent an EV charge to start (router can have lower priority than an EV box to consume available production excess)
+
+**A measurement device is required to use these features.**
+
+## OSS vs PRO
+
+OSS and Pro firmware are the same, except that the PRO version relies on commercial (paid) libraries and provides some additional features based on a better dashboard.
+
+**The Pro version is only 25 euros** and gives access to all the perks of the Pro version below:
+
+| Feature | OSS (Free) | PRO (Paid) |
+| -------------------------------- | :--------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| Dashboard | Overview **only** | Full Dashboard as seen in the screenshots |
+| Manual Dimmer Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Manual Bypass Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Manual Relay Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API |
+| Configuration | Debug Config Page | **From Dashboard**
Debug Config Page |
+| Automatic Resistance Calibration | ❌ | ✅ |
+| Energy Reset | ❌ | ✅ |
+| GPIO Config and Health | ❌ | ✅ |
+| Hardware Config and Health | ❌ | ✅ |
+| Output Statistics | ❌ | ✅ |
+| PID Tuning View | ❌ | ✅ |
+| PZEM Pairing | ❌ | ✅ |
+| Help & Support | [Facebook Group](https://www.facebook.com/groups/yasolr) | [Facebook Group](https://www.facebook.com/groups/yasolr)
[Forum](https://github.com/mathieucarbou/YaSolR-OSS/discussions)
[Bug Report](https://github.com/mathieucarbou/YaSolR-OSS/issues) |
+| Web Console | [WebSerial Lite](https://github.com/mathieucarbou/WebSerialLite) | [WebSerial Pro](https://www.webserial.pro) |
+| Dashboard | [ESP-DASH](https://github.com/ayushsharma82/ESP-DASH) | [ESP-DASH Pro](https://espdash.pro) |
+| OTA Firmware Update | [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) | [ElegantOTA Pro](https://elegantota.pro) |
+
+The money helps funding the hardware necessary to test and develop the firmware.
+
+## Alternatives and Inspirations
+
+This project was inspired by the following awesome Solar Router projects:
+
+- [Routeur Solaire Apper](https://ota.apper-solaire.org) (Cyril P., _[xlyric](https://github.com/xlyric)_)
+- [Routeur Solaire PVbrain](https://github.com/SeByDocKy/pvbrain2) (SĂ©bastien P., _[SeByDocKy](https://github.com/SeByDocKy)_)
+- [Routeur Solaire MK2 PV Router](https://www.mk2pvrouter.co.uk) (Robin Emley)
+- [Routeur Solaire Mk2 PV Router](https://github.com/FredM67/Mk2PVRouter) (Frédéric M.)
+- [Routeur Solaire Tignous](https://forum-photovoltaique.fr/viewtopic.php?f=110&t=40512) (Tignous)
+- [Routeur Solaire MaxPV](https://github.com/Jetblack31/MaxPV) (Jetblack31)
+- [Routeur solaire "Le Profes'Solaire"](https://sites.google.com/view/le-professolaire/routeur-professolaire) (Anthony G., _Le Profes'Solaire_)
+- [Routeur solaire "Le Profes'Solaire"](https://github.com/benoit-cty/solar-router) (Adapation from Benoit C.)
+- [Routeur solaire Multi-Sources Multi-Modes et Modulaire](https://f1atb.fr/fr/realisation-dun-routeur-photovoltaique-multi-sources-multi-modes-et-modulaire/) (André B., _[F1ATB](https://github.com/F1ATB)_)
+- [Routeur solaire ESP Home](https://domo.rem81.com/index.php/2023/07/18/pv-routeur-solaire/) (Remy)
diff --git a/docs/learn/APRIMA_-_CEM_2014-03-27.pdf b/docs/learn/APRIMA_-_CEM_2014-03-27.pdf
new file mode 100644
index 0000000..38287e2
Binary files /dev/null and b/docs/learn/APRIMA_-_CEM_2014-03-27.pdf differ
diff --git a/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf b/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf
new file mode 100644
index 0000000..8aceaf2
Binary files /dev/null and b/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf differ
diff --git a/docs/learn/CEI 61000-3-2.pdf b/docs/learn/CEI 61000-3-2.pdf
new file mode 100644
index 0000000..b3f135b
Binary files /dev/null and b/docs/learn/CEI 61000-3-2.pdf differ
diff --git a/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf b/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf
new file mode 100644
index 0000000..2019573
Binary files /dev/null and b/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf differ
diff --git a/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf b/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf
new file mode 100644
index 0000000..68edc1b
Binary files /dev/null and b/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf differ
diff --git a/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf b/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf
new file mode 100644
index 0000000..e0ef9b2
Binary files /dev/null and b/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf differ
diff --git "a/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf" "b/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf"
new file mode 100644
index 0000000..3dd85c9
Binary files /dev/null and "b/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf" differ
diff --git a/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf b/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf
new file mode 100644
index 0000000..94dc65e
Binary files /dev/null and b/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf differ
diff --git a/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf b/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf
new file mode 100644
index 0000000..86a6dc4
Binary files /dev/null and b/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf differ
diff --git a/docs/learn/RC Snubber for TRIAC.pdf b/docs/learn/RC Snubber for TRIAC.pdf
new file mode 100644
index 0000000..f90c94e
Binary files /dev/null and b/docs/learn/RC Snubber for TRIAC.pdf differ
diff --git a/docs/learn/Relais SSR.txt b/docs/learn/Relais SSR.txt
new file mode 100644
index 0000000..c49e6a4
--- /dev/null
+++ b/docs/learn/Relais SSR.txt
@@ -0,0 +1,2 @@
+C'est le type de MOC qui définit le Random ou pas. Les 3021, 3023 (400 Volt), 3053 (600 Volt), 3052, 3073 (800V) sont "non zéro crossing".
+Donc changement obligatoire s'ils sont équipés d'un MOC 3083, 3063, ... qui sont "zéro crossing"
\ No newline at end of file
diff --git a/docs/learn/Simulation/Simulation.png b/docs/learn/Simulation/Simulation.png
new file mode 100644
index 0000000..f6d0938
Binary files /dev/null and b/docs/learn/Simulation/Simulation.png differ
diff --git a/docs/learn/Simulation/TRIAC_ST_AKG.asy b/docs/learn/Simulation/TRIAC_ST_AKG.asy
new file mode 100644
index 0000000..8f9fe39
--- /dev/null
+++ b/docs/learn/Simulation/TRIAC_ST_AKG.asy
@@ -0,0 +1,28 @@
+Version 4
+SymbolType CELL
+LINE Normal 0 44 36 44
+LINE Normal 0 20 36 20
+LINE Normal 36 20 20 44
+LINE Normal 4 20 20 44
+LINE Normal 32 0 32 20
+LINE Normal 32 44 32 64
+LINE Normal 28 44 64 44
+LINE Normal 28 44 44 20
+LINE Normal 44 20 60 44
+LINE Normal 36 20 64 20
+LINE Normal 0 64 -16 64
+LINE Normal 0 64 20 44
+WINDOW 0 48 0 Left 2
+WINDOW 3 48 72 Left 2
+SYMATTR Value TRIAC_ST_AKG
+SYMATTR Prefix X
+SYMATTR Description Generic TRIAC symbol for use with a model that you supply.
+PIN 32 0 NONE 0
+PINATTR PinName A
+PINATTR SpiceOrder 1
+PIN -16 64 NONE 0
+PINATTR PinName G
+PINATTR SpiceOrder 3
+PIN 32 64 NONE 0
+PINATTR PinName K
+PINATTR SpiceOrder 2
diff --git a/docs/learn/Simulation/infos.txt b/docs/learn/Simulation/infos.txt
new file mode 100644
index 0000000..ef7757d
--- /dev/null
+++ b/docs/learn/Simulation/infos.txt
@@ -0,0 +1,27 @@
+Avatar du membremichelg34
+Messages : 769
+Enregistré le : 29 nov. 2021 14:42
+Departement/Region : 34
+Professionnel PV : Non
+Contact :
+Re: Router via TRIAC et "Pollution" du réseau
+par michelg34 » 01 oct. 2023 10:27
+
+Si vous voulez expérimenter par vous-même, il vaut mieux commencer par faire des simulations, c'est moins coûteux et moins dangereux... ;)
+
+VoilĂ un exemple avec LTspice (simulateur SPICE gratuit dispo ici https://www.analog.com/en/design-center/design-tools-and-calculators/ltspice-simulator.html)
+
+ triac_LTspice_view_fft.zip
+(12.11 Kio) Téléchargé 2 fois
+
+
+Extraire les fichiers, ouvrir avec LTSpice le schéma (fichier .asc), lancer la simulation (Simulate->Run), activer la fenêtre de resultat (.wav) en cliquant dessus, puis faire une FFT (View->FFT, puis choisir I(L3)). Cela devrait afficher la pollution harmonique.
+
+image_2023-10-01_102316984.png
+
+
+La simulation est exécutée 3x en variant une inductance L2 mise en sortie du triac, entre rien (1uH pour le fil) et des valeurs déjà élevées.
+Après y-a plus qu'à bricoler/simuler avec des idées de génie pour tenter d'améliorer les choses...
+Bon courage.
+:sun:
+6x Trina Solar Vertex S 395W + 3x APS DS3-L, toiture 17deg sud-ouest, latitude 43.6deg nord
\ No newline at end of file
diff --git a/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf b/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf
new file mode 100644
index 0000000..266b043
Binary files /dev/null and b/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf differ
diff --git a/docs/learn/Simulation/st_standard_snubberless_triacs.lib b/docs/learn/Simulation/st_standard_snubberless_triacs.lib
new file mode 100644
index 0000000..fdf6e4f
--- /dev/null
+++ b/docs/learn/Simulation/st_standard_snubberless_triacs.lib
@@ -0,0 +1,1768 @@
+* File : standard_snubberless_triacs.lib
+* Revision : 10.0
+* Date : 21/01/2021
+*
+**********************************************************************
+* Please Read Carefully:
+*Information in this document is provided solely in connection with ST products. STMicroelectronics NV and its subsidiaries (“ST”) reserve the
+*right to make changes, corrections, modifications or improvements, to this document, and the products and services described herein at any
+*time, without notice.
+*All ST products are sold pursuant to ST’s terms and conditions of sale.
+*Purchasers are solely responsible for the choice, selection and use of the ST products and services described herein, and ST assumes no
+*liability whatsoever relating to the choice, selection or use of the ST products and services described herein.
+*No license, express or implied, by estoppel or otherwise, to any intellectual property rights is granted under this document. If any part of this
+*document refers to any third party products or services it shall not be deemed a license grant by ST for the use of such third party products
+*or services, or any intellectual property contained therein or considered as a warranty covering the use in any manner whatsoever of such
+*third party products or services or any intellectual property contained therein.
+*UNLESS OTHERWISE SET FORTH IN ST’S TERMS AND CONDITIONS OF SALE ST DISCLAIMS ANY EXPRESS OR IMPLIED
+*WARRANTY WITH RESPECT TO THE USE AND/OR SALE OF ST PRODUCTS INCLUDING WITHOUT LIMITATION IMPLIED
+*WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE (AND THEIR EQUIVALENTS UNDER THE LAWS
+*OF ANY JURISDICTION), OR INFRINGEMENT OF ANY PATENT, COPYRIGHT OR OTHER INTELLECTUAL PROPERTY RIGHT.
+*UNLESS EXPRESSLY APPROVED IN WRITING BY AN AUTHORIZED ST REPRESENTATIVE, ST PRODUCTS ARE NOT
+*RECOMMENDED, AUTHORIZED OR WARRANTED FOR USE IN MILITARY, AIR CRAFT, SPACE, LIFE SAVING, OR LIFE SUSTAINING
+*APPLICATIONS, NOR IN PRODUCTS OR SYSTEMS WHERE FAILURE OR MALFUNCTION MAY RESULT IN PERSONAL INJURY,
+*DEATH, OR SEVERE PROPERTY OR ENVIRONMENTAL DAMAGE. ST PRODUCTS WHICH ARE NOT SPECIFIED AS "AUTOMOTIVE
+*GRADE" MAY ONLY BE USED IN AUTOMOTIVE APPLICATIONS AT USER’S OWN RISK.
+*Resale of ST products with provisions different from the statements and/or technical features set forth in this document shall immediately void
+*any warranty granted by ST for the ST product or service described herein and shall not create or extend in any manner whatsoever, any
+*liability of ST.
+*ST and the ST logo are trademarks or registered trademarks of ST in various countries.
+*Information in this document supersedes and replaces all information previously supplied.
+*The ST logo is a registered trademark of STMicroelectronics. All other names are the property of their respective owners.
+*© 2008 STMicroelectronics - All rights reserved
+*STMicroelectronics group of companies
+*Australia - Belgium - Brazil - Canada - China - Czech Republic - Finland - France - Germany - Hong Kong - India - Israel - Italy - Japan -
+*Malaysia - Malta - Morocco - Singapore - Spain - Sweden - Switzerland - United Kingdom - United States of America
+*www.st.com
+**********************************************************************
+*
+***************************************************************************
+* TRIACs PSpice Models *
+***************************************************************************
+* Note :
+*
+* This TRIAC model simulates:
+* -Igt (the same for all quadrants) MAX of the specification
+*note: for 4 quadrants TRIAC, IGT Q4 is taken into account for all quadrants
+* -Il (the same for all quadrants) Typ of the specification
+* -Ih (the same for both polarity) Typ of the specification
+* -VDRM
+* -VRRM
+* -(dI/dt)c and (dV/dt)c parameters are simulated only if those
+* constraints exceed very highly the specified limits.
+* -Power dissipation is realistic and correspond to a typical TRIAC
+*
+* All these parameters are constant, and don't vary neither with
+* temperature nor other parameters.
+*
+* The "STANDARD" parameter switch between 4 quadrants TRIACs (STANDARD = 1)
+* and 3 quadrants TRIACs (STANDARD = 0).
+* The "STANDARD" parameter maintains or suppress the triggering possibility of
+* the TRIAC in the fourth quadrant, and has absolutely NO EFFECT on other
+* parameters.
+*
+* For a correct triac behavior, the "Maximum step size" must be below
+* or equal 20µs.
+*
+*
+*
+*$
+.subckt Triac_ST A K G PARAMS:
++ Vdrm=400v
++ Igt=20ma
++ Ih=6ma
++ Rt=0.01
++ Standard=1
+*
+* Vdrm : Repetitive forward off-state voltage
+* Ih : Holding current
+* Igt : Gate trigger current
+* Rt : Dynamic on-state resistance
+* Standard : Differenciation between Snubberless and Standard TRIACs
+* (Standard=0 => Snubberless TRIACs, Standard=1 => Standard TRIACs)
+*
+****************************
+* Power circuit *
+****************************
+*
+****************************
+*Switch circuit*
+****************************
+* Q1 & Q2 Conduction
+S_S3 A Plip1 positive 0 Smain
+*RS_S3 positive 0 1G
+D_DAK1 Plip1 Plip2 Dak
+R_Rlip Plip1 Plip2 1k
+V_Viak Plip2 K DC 0 AC 0
+*
+* Q3 & Q4 Conduction
+S_S4 A Plin1 negative 0 Smain
+*RS_S4 negative 0 1G
+D_DKA1 Plin2 Plin1 Dak
+R_Rlin Plin1 Plin2 1k
+V_Vika K Plin2 DC 0 AC 0
+****************************
+*Gate circuit*
+****************************
+R_Rgk G K 10G
+D_DGKi Pg2 G Dgk
+D_DGKd G Pg2 Dgk
+V_Vig Pg2 K DC 0 AC 0
+R_Rlig G Pg2 1k
+*
+****************************
+*Interface circuit*
+****************************
+* positive pilot
+R_Rp Controlp positive 2.2
+C_Cp 0 positive 1u
+E_IF15OR3 Controlp 0 VALUE {IF( ( (V(CMDIG)>0.5) | (V(CMDILIH)>0.5) | (V(CMDVdrm)>0.5) ),400,0 )}
+*
+* negative pilot
+R_Rn Controln negative 2.2
+C_Cn 0 negative 1u
+E_IF14OR3 Controln 0 VALUE {IF( ( (V(CMDIG)>0.5) | (V(CMDILIHN)>0.5) | (V(CMDVdrm)>0.5) ),400,0 )}
+*
+****************************
+* Pilots circuit *
+****************************
+****************************
+* Pilot Gate *
+****************************
+E_IF1IG inIG 0 VALUE {IF( ( ABS(I(V_Vig)) ) > (Igt-1u) ,1,0 )}
+E_MULT2MULT CMDIG 0 VALUE {V(Q4)*V(inIG)}
+E_IF2Quadrant4 Q4 0 VALUE {IF(((I(V_Vig)>(Igt-0.000001))&((V(A)-V(K))<0)&(Standard==0)),0,1)}
+*
+****************************
+* Pilot IHIL *
+****************************
+*
+E_IF10IL inIL 0 VALUE {IF( ((I(V_Viak))>(Ih/2)),1,0 )}
+E_IF5IH inIH 0 VALUE {IF( ((I(V_Viak))>(Ih/3)),1,0 )}
+*
+* Flip_flop IHIL
+E_IF6DIHIL SDIHIL 0 VALUE {IF((V(inIL)*V(inIH)+V(inIH)*(1-V(inIL))*(V(CMDILIH)) )>0.5,1,0)}
+C_CIHIL CMDILIH 0 1n
+R_RIHIL SDIHIL CMDILIH 1K
+R_RIHIL2 CMDILIH 0 100Meg
+*
+****************************
+* Pilot IHILN *
+****************************
+*
+E_IF11ILn inILn 0 VALUE {IF( ((I(V_Vika))>(Ih/2)),1,0 )}
+E_IF3IHn inIHn 0 VALUE {IF( ((I(V_Vika))>(Ih/3)),1,0 )}
+* Flip_flop IHILn
+E_IF4DIHILN SDIHILN 0 VALUE {IF((V(inILn)*V(inIHn)+V(inIHn)*(1-V(inILn))*(V(CMDILIHN)) )>0.5,1,0)}
+C_CIHILn CMDILIHN 0 1n
+R_RIHILn SDIHILN CMDILIHN 1K
+R_RIHILn2 CMDILIHN 0 100Meg
+*
+****************************
+* Pilot VDRM *
+****************************
+E_IF8Vdrm inVdrm 0 VALUE {IF( (ABS(V(A)-V(K))>(Vdrm*1.3)),1,0 )}
+E_IF9IHVDRM inIhVdrm 0 VALUE {IF( (I(V_Viak)>(Vdrm*1.3)/1.2meg)| (I(V_Vika)>(Vdrm*1.3)/1.2meg),1,0)}
+* Flip_flop VDRM
+E_IF7DVDRM SDVDRM 0 VALUE {IF((V(inVdrm)+(1-V(inVdrm))*V(inIhVdrm)*V(CMDVdrm) )>0.5,1,0)}
+C_CVdrm CMDVdrm 0 1n
+R_RVdrm SDVDRM CMDVdrm 100
+R_RVdrm2 CMDVdrm 0 100Meg
+*
+****************************
+* Switch Model *
+****************************
+.MODEL Smain VSWITCH Roff=1.2meg Ron={Rt} Voff=0 Von=100
+****************
+* Diodes Model *
+****************
+.MODEL Dak D( Is=3E-12 Cjo=5pf)
+.MODEL Dgk D( Is=1E-16 Cjo=50pf Rs=5)
+.ends
+*
+*********************************************************************
+* TRIACs PSpice Library *
+*********************************************************************
+*********************************************************************
+* Standard TRIACs definition *
+*********************************************************************
+*
+*$
+.subckt T4050-6 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=85ma
++ Rt=0.010
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T405-700 A K G
+X1 A K G Triac_ST params:
++ Vdrm=700v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.120
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T435-700 A K G
+X1 A K G Triac_ST params:
++ Vdrm=700v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.120
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T835-8G A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.05
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T850-8G A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.05
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T850-6G A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.05
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T1210-6G A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.035
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T1050-8G A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.040
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T1035-6G A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.040
++ Standard=1
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T830-8FP A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=30ma
++ Ih=50ma
++ Rt=0.04
++ Standard=1
+* 2015 / ST / Rev 0
+.ends
+*$
+.subckt Z00607MA A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=7ma
++ Ih=5ma
++ Rt=0.42
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0103M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=7ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0103N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=5ma
++ Ih=7ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0107M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=7ma
++ Ih=10ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0107N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=7ma
++ Ih=10ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0109M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=10ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0109N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=10ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0110M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=25ma
++ Ih=25ma
++ Rt=0.4
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt Z0402M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=3ma
++ Ih=3ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0405M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=5ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0405N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=5ma
++ Ih=5ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0409M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=10ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0409N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=10ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0410M A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=25ma
++ Ih=25ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt Z0410N A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=25ma
++ Ih=25ma
++ Rt=0.18
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB04-600SL A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=25ma
++ Ih=15ma
++ Rt=0.1
++ Standard=1
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T405Q-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=10ma
++ Rt=0.1
++ Standard=1
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA06-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.06
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-800C A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.06
++ Standard=1
+* 2013 / ST / Rev 0
+.ends
+*$
+.subckt BTB06-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.06
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.06
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB06-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.06
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA08-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.05
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA08-800C A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.05
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.05
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-800C A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.05
++ Standard=1
+* 2013 / ST / Rev 0
+.ends
+*$
+.subckt BTA08-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.05
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.05
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA10-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.04
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA10-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.04
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA12-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.035
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA16-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.025
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA16-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.025
++ Standard=1
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA16-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.025
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB16-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.025
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB16-600C A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=25ma
++ Rt=0.025
++ Standard=1
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTB16-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=50ma
++ Rt=0.025
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB24-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB24-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA25-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA25-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA26-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA26-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB26-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.016
++ Standard=1
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA40-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.010
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA40-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.010
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA41-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.01
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA41-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.01
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB41-600B A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.01
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB41-800B A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=80ma
++ Rt=0.01
++ Standard=1
+* 1999 / ST / Rev 0
+.ends
+*********************************************************************
+* Snubberless & Logic Level Triac definition *
+*********************************************************************
+*$
+.subckt T2650-6PF A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.014
++ Standard=0
+* 2021 / ST / Rev 0
+.ends
+*$
+.subckt T405-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T405-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T410-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T410-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T435-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T435-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.12
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-600TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB06-600TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB06-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-800TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.06
++ Standard=0
+* 2010 / ST / Rev 0
+.ends
+*$
+.subckt BTB06-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA06-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB06-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.06
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T630-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=30ma
++ Ih=50ma
++ Rt=0.05
++ Standard=0
+* 2010 / ST / Rev 0
+.ends
+*$
+.subckt BTA08-600TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-600TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-800TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA08-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA08-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.056
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB08-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA08-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA08-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB08-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T810-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T810-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T835-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T835-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.05
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA10-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA10-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA10-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA10-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB10-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB10-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.04
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-600TW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=5ma
++ Ih=10ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA12-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-800SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA12-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA12-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB12-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T1210-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T1235-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T1235-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.035
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA16-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA16-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB16-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB16-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA16-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA16-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB16-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTB16-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=50ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt BTA16-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.025
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB16-600SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.025
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB16-800SW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T1610-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.025
++ Standard=0
+* 2010 / ST / Rev 0
+.ends
+*$
+.subckt T1610-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=10ma
++ Ih=15ma
++ Rt=0.025
++ Standard=0
+* 2010 / ST / Rev 0
+.ends
+*$
+.subckt T1620-600W A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=20ma
++ Ih=35ma
++ Rt=0.02
++ Standard=0
+* 2008 / ST / Rev 1
+.ends
+*$
+.subckt T1635-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=35ma
++ Rt=0.025
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA20-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.02
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA24-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA24-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB24-600CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB24-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA24-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA24-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB24-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTB24-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA25-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA25-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T2535-600 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt T2535-800 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 2008 / ST / Rev 0
+.ends
+*$
+.subckt BTA26-800CW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=35ma
++ Ih=50ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA26-600BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt BTA26-800BW A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=50ma
++ Ih=75ma
++ Rt=0.016
++ Standard=0
+* 1999 / ST / Rev 0
+.ends
+*$
+.subckt T2550-12 A K G
+X1 A K G Triac_ST params:
++ Vdrm=1200v
++ Igt=50ma
++ Ih=60ma
++ Rt=0.02
++ Standard=0
+* 2014 / ST / Rev 0
+.ends
+*
+*********************************************************************
+* Alternistors definition *
+*********************************************************************
+*
+*$
+.subckt TXDV812 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=100ma
++ Ih=100ma
++ Rt=0.04
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TXDV1212 A K G
+X1 A K G Triac_ST params:
++ Vdrm=1200v
++ Igt=100ma
++ Ih=100ma
++ Rt=0.04
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV640 A K G
+X1 A K G Triac_ST params:
++ Vdrm=600v
++ Igt=200ma
++ Ih=50ma
++ Rt=0.012
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV840 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=200ma
++ Ih=50ma
++ Rt=0.012
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV1240 A K G
+X1 A K G Triac_ST params:
++ Vdrm=1200v
++ Igt=200ma
++ Ih=50ma
++ Rt=0.012
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV825 A K G
+X1 A K G Triac_ST params:
++ Vdrm=800v
++ Igt=150ma
++ Ih=50ma
++ Rt=0.019
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV1025 A K G
+X1 A K G Triac_ST params:
++ Vdrm=1000v
++ Igt=150ma
++ Ih=50ma
++ Rt=0.019
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
+.subckt TPDV1225 A K G
+X1 A K G Triac_ST params:
++ Vdrm=1200v
++ Igt=150ma
++ Ih=50ma
++ Rt=0.019
++ Standard=0
+* 2011 / ST / Rev 0
+.ends
+*$
diff --git a/docs/learn/Simulation/triac23_filter.asc b/docs/learn/Simulation/triac23_filter.asc
new file mode 100644
index 0000000..a645101
--- /dev/null
+++ b/docs/learn/Simulation/triac23_filter.asc
@@ -0,0 +1,98 @@
+Version 4
+SHEET 1 1340 756
+WIRE -816 -288 -1120 -288
+WIRE -176 -288 -736 -288
+WIRE 256 -288 -176 -288
+WIRE 560 -288 256 -288
+WIRE 560 -160 560 -288
+WIRE 752 -160 560 -160
+WIRE 752 -128 752 -160
+WIRE 256 -112 256 -288
+WIRE -176 -80 -176 -288
+WIRE 512 -80 400 -80
+WIRE 560 -80 560 -160
+WIRE 400 -48 400 -80
+WIRE 752 -16 752 -48
+WIRE 400 64 400 32
+WIRE 560 80 560 -16
+WIRE 752 80 752 48
+WIRE 752 80 560 80
+WIRE 48 112 0 112
+WIRE 112 112 48 112
+WIRE 256 112 256 -32
+WIRE 256 112 192 112
+WIRE 336 112 256 112
+WIRE 0 144 0 112
+WIRE -1120 176 -1120 -288
+WIRE -176 256 -176 0
+WIRE 0 256 0 224
+WIRE 0 256 -176 256
+WIRE 400 256 400 160
+WIRE 400 256 0 256
+WIRE 560 352 560 80
+WIRE 656 352 560 352
+WIRE 1280 352 736 352
+WIRE 1280 464 1280 352
+WIRE -1120 672 -1120 256
+WIRE 1280 672 1280 544
+WIRE 1280 672 -1120 672
+WIRE -1120 720 -1120 672
+FLAG 48 112 cde
+FLAG -1120 720 0
+SYMBOL voltage -176 -96 R0
+WINDOW 123 0 0 Left 0
+WINDOW 39 0 0 Left 0
+SYMATTR InstName V1
+SYMATTR Value 5
+SYMBOL voltage 0 128 R0
+WINDOW 3 -36 160 Left 2
+WINDOW 123 0 0 Left 0
+WINDOW 39 0 0 Left 0
+SYMATTR Value PULSE(5 0 5m 10n 10n 100u 10m)
+SYMATTR InstName V2
+SYMBOL res 736 -144 R0
+SYMATTR InstName R1
+SYMATTR Value 100
+SYMBOL cap 736 -16 R0
+SYMATTR InstName C1
+SYMATTR Value 1n
+SYMBOL res 384 -64 R0
+SYMATTR InstName R2
+SYMATTR Value 47
+SYMBOL pnp 336 160 M180
+SYMATTR InstName Q1
+SYMATTR Value BC327-25
+SYMBOL res 240 -128 R0
+SYMATTR InstName R4
+SYMATTR Value 10k
+SYMBOL voltage -1120 160 R0
+WINDOW 123 0 0 Left 0
+WINDOW 39 0 0 Left 0
+SYMATTR InstName V3
+SYMATTR Value SINE(0 325 50)
+SYMBOL res 1264 448 R0
+SYMATTR InstName R3
+SYMATTR Value 23
+SYMBOL TRIAC_ST_AKG 528 -16 M180
+SYMATTR InstName U1
+SYMATTR Value BTA26-800CW
+SYMBOL res 208 96 R90
+WINDOW 0 0 56 VBottom 2
+WINDOW 3 32 56 VTop 2
+SYMATTR InstName R5
+SYMATTR Value 10
+SYMBOL ind 752 336 R90
+WINDOW 0 5 56 VBottom 2
+WINDOW 3 32 56 VTop 2
+SYMATTR InstName L2
+SYMATTR Value {L}
+SYMBOL ind -832 -272 R270
+WINDOW 0 32 56 VTop 2
+WINDOW 3 5 56 VBottom 2
+SYMATTR InstName L3
+SYMATTR Value 50µ
+SYMATTR SpiceLine Rser=10m
+TEXT 640 168 Left 2 !.inc st_standard_snubberless_triacs.lib
+TEXT 144 344 Left 2 !.tran 1000m
+TEXT -1104 -376 Left 2 ;cable BT R+jX , X=Lw=0.08ohm/km = 250uH/km (2x aller et retour)
+TEXT 16 384 Left 2 !.step param L list 1u 1600u 4700u
diff --git a/docs/learn/Simulation/triac23_filter.plt b/docs/learn/Simulation/triac23_filter.plt
new file mode 100644
index 0000000..7348830
Binary files /dev/null and b/docs/learn/Simulation/triac23_filter.plt differ
diff --git a/docs/learn/Solid State Relay Guide - Phidgets Support.pdf b/docs/learn/Solid State Relay Guide - Phidgets Support.pdf
new file mode 100644
index 0000000..5c909f6
Binary files /dev/null and b/docs/learn/Solid State Relay Guide - Phidgets Support.pdf differ
diff --git a/docs/learn/Solid State Relays.pdf b/docs/learn/Solid State Relays.pdf
new file mode 100644
index 0000000..9b9ae45
Binary files /dev/null and b/docs/learn/Solid State Relays.pdf differ
diff --git a/docs/learn/TRIAC.pdf b/docs/learn/TRIAC.pdf
new file mode 100644
index 0000000..aa63445
Binary files /dev/null and b/docs/learn/TRIAC.pdf differ
diff --git a/docs/learn/introduction_to_pid_controllers_ed2.pdf b/docs/learn/introduction_to_pid_controllers_ed2.pdf
new file mode 100644
index 0000000..ee874c8
Binary files /dev/null and b/docs/learn/introduction_to_pid_controllers_ed2.pdf differ
diff --git a/docs/learn/zdc.txt b/docs/learn/zdc.txt
new file mode 100644
index 0000000..4d293bb
--- /dev/null
+++ b/docs/learn/zdc.txt
@@ -0,0 +1,4 @@
+- https://www.bristolwatch.com/ele2/zcnew.htm
+- https://www.electronics-lab.com/project/ac-voltage-zero-cross-detector/
+- https://dextrel.net/dextrel-start-page/design-ideas-2/mains-zero-crossing-detector
+- https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html
diff --git a/docs/manual.md b/docs/manual.md
new file mode 100644
index 0000000..6c97088
--- /dev/null
+++ b/docs/manual.md
@@ -0,0 +1,795 @@
+---
+layout: default
+title: Manual
+description: Manual
+---
+
+# YaSolR Manual
+
+- [Quick Start](#quick-start)
+ - [Initial Firmware Installation](#initial-firmware-installation)
+ - [Captive Portal (Access Point) and WiFi](#captive-portal-access-point-and-wifi)
+ - [Access Point Mode](#access-point-mode)
+- [Dashboard](#dashboard)
+ - [`/config` page](#config-page)
+ - [`/console` page](#console-page)
+ - [`/update` page](#update-page)
+ - [`Overview` section](#overview-section)
+ - [`Output` sections](#output-sections)
+ - [`Relays` section](#relays-section)
+ - [`Management` section](#management-section)
+ - [`Network` section](#network-section)
+ - [`MQTT` section](#mqtt-section)
+ - [MQTT as a Grid Source](#mqtt-as-a-grid-source)
+ - [Home Assistant Discovery](#home-assistant-discovery)
+ - [MQTT as a Temperature Source](#mqtt-as-a-temperature-source)
+ - [`GPIO` section](#gpio-section)
+ - [`Hardware` section](#hardware-section)
+ - [`Hardware Config` section](#hardware-config-section)
+ - [Grid Frequency](#grid-frequency)
+ - [Display](#display)
+ - [Relays](#relays)
+ - [Resistance Calibration](#resistance-calibration)
+ - [PZEM Pairing](#pzem-pairing)
+ - [`PID Controller` section](#pid-controller-section)
+ - [`Statistics` section](#statistics-section)
+- [Important Hardware Information](#important-hardware-information)
+ - [Bypass Relay](#bypass-relay)
+ - [Display](#display)
+ - [JSY-MK-194T (local)](#jsy-mk-194t-local)
+ - [JSY-MK-194T (remote)](#jsy-mk-194t-remote)
+ - [LEDs](#leds)
+ - [Temperature Sensor](#temperature-sensor)
+ - [Zero-Cross Detection](#zero-cross-detection)
+ - [Virtual Grid Power / Compatibility with EV box](#virtual-grid-power--compatibility-with-ev-box)
+- [Help and support](#help-and-support)
+
+## Quick Start
+
+When everything is wired and installed properly, you can:
+
+1. Flash the downloaded firmware (see [Initial Firmware Installation](#initial-firmware-installation))
+2. Power on the system to start the application
+3. Connect to the WiFI: `YaSolR-xxxxxx`
+4. Connect to the Captive Portal to setup your WiFi (see: [Captive Portal (Access Point) and WiFi](#captive-portal-access-point-and-wifi))
+5. Go to the [GPIO](#gpio-section) page to verify or change your GPIO settings
+6. Go to the [Hardware](#hardware-section) page to activate the hardware you have
+7. Go to the [Hardware Config](#hardware-config-section) page to configure your hardware settings and resistance values.
+ [Resistance Calibration](#resistance-calibration) is really important to do otherwise the router will not work.
+8. Go to the [MQTT](#mqtt-section) page to configure your MQTT settings if needed.
+9. Go to the [Relays](#relays-section) page to configure your relay loads if needed.
+10. Go to [Output 1 & 2](#output-sections) pages to configure your bypass options and dimmer settings if needed.
+11. Restart to activate everything.
+12. Enjoy your YaSolR!
+
+### Initial Firmware Installation
+
+**The firmware file which must be used for a first installation is the one ending with `.FACTORY.bin`.**
+
+Firmware can be downloaded here : [![Download](https://img.shields.io/badge/Download-firmware-green.svg)](https://yasolr.carbou.me/download)
+
+Flash with `esptool.py` (Linux / MacOS):
+
+```bash
+# Erase the memory (including the user data)
+esptool.py \
+ --port /dev/ttyUSB0 \
+ erase_flash
+```
+
+```bash
+# Flash initial firmware and partitions
+esptool.py \
+ --port /dev/ttyUSB0 \
+ --chip esp32 \
+ --before default_reset \
+ --after hard_reset \
+ write_flash \
+ --flash_mode dout \
+ --flash_freq 40m \
+ --flash_size detect \
+ 0x0 YaSolR-VERSION-MODEL-CHIP.FACTORY.bin
+```
+
+Do not forget to change the port `/dev/ttyUSB0` to the one matching your system.
+For example on Mac, it is often `/dev/cu.usbserial-0001` instead of `/dev/ttyUSB0`.
+
+With [Espressif Flash Tool](https://www.espressif.com/en/support/download/other-tools) (Windows):
+
+> ##### IMPORTANT
+> Be careful to not forget the `0`
+{: .block-important }
+
+![Espressif Flash Tool](assets/img/screenshots/Espressif_Flash_Tool.png)
+
+### Captive Portal (Access Point) and WiFi
+
+
+> ##### TIP
+> Captive Portal and Access Point address: [http://192.168.4.1/](http://192.168.4.1/)
+{: .block-tip }
+
+A captive portal (Access Point) is started for the first time to configure the WiFi network, or when the application starts and cannot join an already configured WiFi network fro 15 seconds.
+
+![](assets/img/screenshots/Captive_Portal.jpeg)
+
+The captive portal is only started for 3 minutes, to allow configuring a (new) WiFi network.
+After this delay, the portal will close, and the application will try to connect again to the WiFi.
+And again, if the WiFi cannot be reached, connected to, or is not configured, the portal will be started again.
+
+This behavior allows to still have access to the application in case of a WiFi network change, or after a power failure, when the application restarts.
+If the application restarts before the WiFi is available, it will launch the portal for 3 minutes, then restart and try to join the network again.
+
+In case of WiFi disruption (WiFi temporary down), the application will keep trying to reconnect.
+If it is restarted and the WiFi is still not available, the Captive Portal will be launched.
+
+### Access Point Mode
+
+You can also chose to not connect to your Home WiFi and keep the AP mode active.
+In this case, you will need to connect to the router WiFi each time you want to access it.
+
+In AP mode, all the features depending on Internet access and time are not available (MQTT, NTP).
+You will have to manually sync the time from your browser to activate the auto bypass feature.
+
+## Dashboard
+
+Here are the main links to know about in the application:
+
+- `http://yasolr.local/`: Dashboard
+- `http://yasolr.local/console`: Web Console
+- `http://yasolr.local/update`: Web OTA (firmware update)
+- `http://yasolr.local/config`: Debug Configuration Page
+- `http://yasolr.local/api`: [REST API](rest)
+
+_(replace `yasolr.local` with the IP address of the router)_
+
+### `/config` page
+
+This page is accessible at: `http:///config`.
+It allows to see the raw current configuration of the router and edit it.
+
+[![](assets/img/screenshots/config.jpeg)](assets/img/screenshots/config.jpeg)
+
+> ##### WARNING
+> This page should not normally be used, except for debugging purposes.
+
+### `/console` page
+
+A Web Console is accessible at: `http:///console`.
+You can see more logs if you activate Debug logging (but it will make the router react a bit more slowly).
+
+[![](assets/img/screenshots/console.jpeg)](assets/img/screenshots/console.jpeg)
+
+### `/update` page
+
+Go to the Web OTA at `http:///update` to update the firmware over the air:
+
+[![](assets/img/screenshots/update.jpeg)](assets/img/screenshots/update.jpeg)
+
+The firmware file which must be used is the one ending with `.OTA.bin`:
+
+`YaSolR---.OTA.bin`
+
+### `Overview` section
+
+The overview section shows some global information about the router:
+
+- The temperature is coming from the sensor installed in the router box
+- A JSY or PZEM is required to see the measurements
+
+[![](assets/img/screenshots/overview.jpeg)](assets/img/screenshots/overview.jpeg)
+
+### `Output` sections
+
+The output sections show the state of the outputs and the possibility to control them.
+
+| [![](assets/img/screenshots/output1.jpeg)](assets/img/screenshots/output1.jpeg) | [![](assets/img/screenshots/output2.jpeg)](assets/img/screenshots/output2.jpeg) |
+
+- `Status`
+ - `Disabled`: Output is disabled (dimmer disabled or other reason)
+ - `Idle`: Output is not routing and not in bypass mode
+ - `Routing`: Routing in progress
+ - `Bypass`: Bypass has been activated manually
+ - `Bypass Auto`: Bypass has been activated based on automatic rules
+- `Temperature`: This is the temperature reported by the sensor in water tank, if installed
+
+**Energy:**
+
+- `Power`: Routed power.
+- `Apparent Power`: Apparent power in VA circulating on the wires.
+- `Power Factor`: Power factor (if lower than 1, mainly composed of harmonic component). Ideal is close to 1.
+- `THDi`: This is the estimated level of harmonics generated by this output. The lower, the better.
+- `Voltage`: The dimmed RMS voltage sent to the resistive load.
+- `Current`: The current in Amp sent to the resistive load.
+- `Resistance`: The resistance of the load.
+- `Energy`: The total accumulated energy routed by this output, stored in hardware (JSY and/or PZEM).
+
+> ##### IMPORTANT
+> A PZEM is required to see the measurements of each outputs.
+{: .block-important }
+
+**Dimmer Control:**
+
+- `Dimmer Duty Manual Control`: Slider to control the dimmer level manually.
+ Only available when the dimmer is not in automatic mode.
+ Otherwise the dimmer level is displayed.
+- `Dimmer Duty Limiter`: Slider to limit the level of the dimmer in order to limit the routed power.
+- `Dimmer Temperature Limiter`: Temperature threshold when the dimmer will stop routing. This temperature can be different than the temperature used in auto bypass mode.
+- `Dimmer Automatic Control`: ON/OFF switch to select automatic routing mode or manual control of the dimmer.
+ Resistance calibration step is required before using automatic mode.
+- `Grid Excess Reserved`: Allows to share the remaining grid excess to the second output.
+ Only available in automatic mode.
+ For example, if output 1 is set to 60%, then output 1 will take at most 60% of the grid excess (eventually less if 60% of the grid excess exceeds the nominal power of the connected load).
+ Output 2 will be dimmed with the remaining excess.
+
+**Bypass Control:**
+
+- `Bypass`: Activate or deactivate bypass(force heating)
+ Only available when the bypass is not in automatic mode.
+ Otherwise the bypass state is displayed.
+- `Bypass Automatic Control`: Activate or deactivate automatic bypass based on hours and/or temperature.
+
+The following settings are visible if `Bypass Automatic Control` is activated.
+
+- `Bypass Week Days`: Days of the week when the bypass can be activated.
+- `Bypass Start Time` / `Bypass Stop Time`: The time range when the auto bypass is allowed to start.
+- `Bypass Start Temperature`: The temperature threshold when the auto bypass will start: the temperature of the water tank needs to be lower than this threshold.
+- `Bypass Stop Temperature`: The temperature threshold when the auto bypass will stop: the temperature of the water tank needs to be higher than this threshold.
+
+> ##### TIP
+> All these settings are applied immediately and do not require a restart
+{: .block-tip }
+
+### `Relays` section
+
+YaSolR supports 2 additional relays (Electromechanical or SSR, controlled with 3.3V DC) to control external loads, or to be connected to the A1 and A2 terminals of a power contactor.
+Relays can also be connected to the other resistance of the water tank (tri-phase resistance) as described in the [recommendations to reduce harmonics and flickering](./overview#recommendations-to-reduce-harmonics-and-flickering), in order to improve the routing and reduce harmonics.
+You must use a SSR for that, because the relay will be switched on and off frequently.
+
+> ##### NOTE
+> Remember that the voltage is not dimmed: these are 2 normal relays
+{: .block-note }
+
+[![](assets/img/screenshots/relays.jpeg)](assets/img/screenshots/relays.jpeg)
+
+- `Relay X Automatic Control: Connected Load (Watts)`: You can specify the resistive load power in watts connected to the relay.
+ If you do so, the relay will be activated automatically based on the grid power.
+- `Relay X Manual Control`: ON/OFF switch to control the relay manually.
+ Only available when the relay is not in automatic mode.
+ Otherwise the relay state is displayed.
+
+> ##### WARNING
+> Pay attention that there is little to no hysteresis on the relays.
+> So do not use the automatic feature to switch non-resistive loads such as pumps, electric vehicle chargers, etc.
+> If you need to switch other types of load in a more complex way with some hysteresis or other complex conditions, you can use the MQTT, REST API, Home Assistant or Jeedom to query the `Virtual Power` metric and execute an automation based on this value.
+> The automation can then control the router relays remotely. The relays need to be set in `Manual Control`.
+> Remember that these relays are not power contactors and should not be used to directly control high power loads like an Electric Vehicle charge, a pump, etc.
+{: .block-warning }
+
+> ##### TIP
+>
+> - All these settings are applied immediately and do not require a restart
+>
+> - **For an EV charge control**: see [Virtual Grid Power / Compatibility with EV box](#virtual-grid-power--compatibility-with-ev-box)
+>
+> - **For a pump**: a contactor is recommended which can be coupled with a Shelly EM to activate / deactivate the contactor remotely, and it can be automated by Home Assistant or Jeedom based on the `Virtual Power` metric of this router, but also the hours of day, days of week, depending on the weather, and of course with some hysteresis and safety mechanisms to force the pump ON or OFF depending on some rules.
+{: .block-tip }
+
+**Rules of Automatic Switching**
+
+`Grid Virtual Power` is calculated by the router as `Grid Power - Routed Power`.
+This is the power that would be sent to the grid if the router was not routing any power to the resistive loads.
+
+`Grid Virtual Power` is negative on export and positive on import.
+
+- The relay will automatically start when `Grid Virtual Power + Relay Load <= -3% of Relay Load`.
+ In other words, the relay will automatically start when there is enough excess to absorb both the load connected to the relay plus 3% more of it.
+ When the relay will start, the remaining excess not absorbed by the load will be absorbed by the dimmer.
+
+- The relay will automatically stop when `Grid Virtual Power >= 3% of Relay Threshold`.
+ In other words, the relay will automatically stop when there is no excess anymore but a grid import equal to or more than 3% of the relay threshold.
+ When the relay will stop, there will be some excess again, which will be absorbed by the dimmer.
+
+For a 3000W tri-phase resistance, 3% means 30W per relay because there is 3x 1000W resistances.
+For a 2100W tri-phase resistance, 3% means 21W per relay because there is 3x 700W resistances.
+
+### `Management` section
+
+[![](assets/img/screenshots/management.jpeg)](assets/img/screenshots/management.jpeg)
+
+- `Configuration Backup`: Backup the current configuration of the router.
+- `Configuration Restore`: Restore a previously saved configuration.
+- `OTA Firmware Update`: Go to the firmware update page.
+- `Restart`: Restart the router.
+- `Energy Reset`: Reset the energy stored in all devices (JSY and PZEM) of the router.
+- `Factory Reset`: Reset the router to factory settings and restart it.
+- `Debug`: Activate or deactivate debug logging.
+- `Console`: Go to the Web Console page.
+- `Debug Information`: Link to the API page which will output useful debug information to give to support.
+
+### `Network` section
+
+[![](assets/img/screenshots/network.jpeg)](assets/img/screenshots/network.jpeg)
+
+- `Admin Password`: the password used to access (there is no password by default):
+ - Any Web page, including the [REST API](rest)
+ - The Access Point when activated
+ - The Captive Portal when the router restarts and no WiFi is available
+
+**Time settings:**
+
+- `NTP Server`: the NTP server to use to sync the time
+- `Timezone`: the timezone to use for the router
+- `Sync time with browser`: if the router does not have access to Internet or is not able to sync time (I.e. in AP mode), you can use this button to sync the time with your browser.
+
+**WiFi settings:**
+
+- `WiFi SSID`: the Home WiFi SSID to connect to
+- `WiFi Password`: the Home WiFi password to connect to
+- `Stay in AP Mode`: whether to activate or not the Access Point mode: switching the button will ask the router to stay in AP mode after reboot.
+ You will need to connect to its WiFi to access the dashboard again.
+
+**The ESP32 must be restarted to apply the changes.**
+
+### `MQTT` section
+
+[![](assets/img/screenshots/mqtt.jpeg)](assets/img/screenshots/mqtt.jpeg)
+
+- `Server`: the MQTT broker address
+- `Port`: the MQTT broker port (usually `1883` or `8883` for TLS)
+- `Username`: the MQTT username
+- `Password`: the MQTT password
+- `SSL / TLS`: whether to use TLS or not (false by default). If yes, you must upload the server certificate.
+- `Server Certificate`: when using SSL, you need to upload the server certificate.
+- `Publish Interval`: the interval in seconds between each MQTT publication of the router data.
+ The default value is `5` seconds.
+ No need to restart, it is applied immediately.
+- `Base Topic`: the MQTT topic prefix to use for all the topics published by the router.
+ It is set by default to `yasolr_`.
+ I strongly recommend to keep this default value.
+ The ID won't change except if you change the ESP board.
+
+> ##### IMPORTANT
+> MQTT must be restarted to apply the changes.
+{: .block-important }
+
+#### MQTT as a Grid Source
+
+- `Grid Voltage MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the Grid voltage.
+ **Any measurement device (JSY or JSY Remote) will still have priority over MQTT**.
+
+- `Grid Power MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the Grid power.
+ **It takes precedence over any other source, even a JSY connected to the ESP32**.
+ The reason is that it is impossible to know if the second channel of the JSY is really installed and used to monitor the grid power or not.
+
+> ##### IMPORTANT
+> The ESP32 must be restarted to apply the changes.
+{: .block-important }
+
+MQTT topics are less accurate because depend on the refresh rate of this topic, and an expiration delay of a few seconds is set in order to stop any routing if no update is received in time.
+Also, there is **1 minute expiration delay** after which the values will be considered as invalid.
+
+As a general rule, **do not use MQTT as a grid power source if you have a JSY or Remote JSY**.
+
+#### MQTT as a Temperature Source
+
+MQTT can be used to receive temperature data instead of relying on a connected sensor.
+There is **1 minute expiration delay** after which the temperature will be considered as invalid.
+So this is important to make sure that the topic will be refreshed, otherwise features based on temperature won't work.
+
+- `Output 1 Temperature MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the temperature linked to output 1
+- `Output 2 Temperature MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the temperature linked to output 2
+
+> ##### IMPORTANT
+> The ESP32 must be restarted to apply the changes.
+{: .block-important }
+
+#### Home Assistant Discovery
+
+YaSolR supports Home Assistant Discovery: if configured, it will **automatically create a device** for the Solar Router in Home Assistant under the MQTT integration.
+
+| [![](assets/img/screenshots/ha_disco_1.jpeg)](assets/img/screenshots/ha_disco_1.jpeg) | [![](assets/img/screenshots/ha_disco_2.jpeg)](assets/img/screenshots/ha_disco_2.jpeg) |
+
+- `Home Assistant Integration`: whether to activate or not MQTT Discovery
+- `Home Assistant Discovery Topic`: the MQTT topic prefix to use for all the topics published by the router for Home Assistant Discovery.
+ It is set by default to `homeassistant/discovery`.
+ I strongly recommend to keep this default value and configure Home Assistant to use this topic prefix for Discovery in order to separate state topics from discovery topics.
+
+> ##### IMPORTANT
+> MQTT must be restarted to apply the changes.
+{: .block-important }
+
+The complete reference of the published data in MQTT is available [here](mqtt).
+The published data can be explored with [MQTT Explorer](https://mqtt-explorer.com/).
+
+[![](assets/img/screenshots/mqtt_explorer.jpeg){: height="800" }](assets/img/screenshots/mqtt_explorer.jpeg)
+
+**Activating MQTT Discovery in Home Assistant**
+
+You can read more about Home Assistant Discovery and how to configure it [here](https://www.home-assistant.io/docs/mqtt/discovery/).
+
+Here is a configuration example for Home Assistant to move the published state topics under the `homeassistant/states`:
+
+```yaml
+# https://www.home-assistant.io/integrations/mqtt_statestream
+mqtt_statestream:
+ base_topic: homeassistant/states
+ publish_attributes: true
+ publish_timestamps: true
+ exclude:
+ domains:
+ - persistent_notification
+ - automation
+ - calendar
+ - device_tracker
+ - event
+ - geo_location
+ - media_player
+ - script
+ - update
+```
+
+To configure the discovery topic, you need to go to [http://homeassistant.local:8123/config/integrations/integration/mqtt](http://homeassistant.local:8123/config/integrations/integration/mqtt), then click on `configure`, then `reconfigure` then `next`, then you can enter the discovery prefix `homeassistant/discovery`.
+
+Once done on Home Assistant side and YaSolR side, you should see the Solar Router device appear in Home Assistant in the list of MQTT devices.
+
+### `GPIO` section
+
+This section allows to configure the pinout for the connected hardware and get some validation feedback.
+
+[![](assets/img/screenshots/gpio.jpeg)](assets/img/screenshots/gpio.jpeg)
+
+- Set the value to **-1** to disable the pin.
+- Set the input to **blank** and save to reset the pin to its default value.
+
+If you see a warning with `(Input Only)`, it means that this configured pin can only be used to read
+data.
+It perfectly OK for a ZCD, but you cannot use a pin that can only be read for a relay, DS18 sensor, etc.
+
+> ##### IMPORTANT
+> If you change one of these settings, please stop and restart the corresponding Hardware.
+{: .block-important }
+
+### `Hardware` section
+
+This section allows to enable / disable some features of the router, and get some feedback in case some activated features cannot be activated.
+
+[![](assets/img/screenshots/hardware.jpeg)](assets/img/screenshots/hardware.jpeg)
+
+All these components are activated **live without the need to restart the router**.
+
+> ##### NOTE
+>
+> - `Output 1 Relay` / `Output 2 Relay`: these are the SSR or Electromechanical relays connected to the ESP32 and used whn you activate bypass mode.
+> Only activate if you have connected some relays to be used for the output bypass.
+>
+> - `Relay 1` / `Relay 2`: these are the SSR or Electromechanical relays connected to the ESP32 and used to control external loads.
+> Only activate if you have connected some relays to be used for external loads.
+{: .block-note }
+
+### `Hardware Config` section
+
+This section allows to further configure some hardware settings and calibrate the resistance values of the loads.
+
+[![](assets/img/screenshots/hardware_config.jpeg)](assets/img/screenshots/hardware_config.jpeg)
+
+> ##### IMPORTANT
+> If you change one of these settings in the hardware section, please restart the corresponding hardware or the YaSolR device.
+{: .block-important }
+
+#### Grid Frequency
+
+- `Nominal Grid Frequency`: the nominal grid frequency.
+
+#### Display
+
+- `Display Speed`: the speed at which the display will switch to the next page.
+ This setting is applied immediately and does not require a restart.
+- `Display Type`: the type of display used.
+- `Display Rotation`: the rotation of the display.
+
+#### Relays
+
+- `Output 1 Relay Type (Bypass)`: the relay type for Output 1 Bypass: Normally Open (NO) or Normally Closed (NC).
+- `Output 2 Relay Type (Bypass)`: the relay type for Output 2 Bypass: Normally Open (NO) or Normally Closed (NC).
+- `Relay 1 Type`: the relay type for Relay 1: Normally Open (NO) or Normally Closed (NC).
+- `Relay 2 Type`: the relay type for Relay 2: Normally Open (NO) or Normally Closed (NC).
+
+#### Resistance Calibration
+
+**The router needs to know the resistance value of the load to correctly calculate the dimmer values**.
+
+- `Output 1 Resistance`: the resistance value in Ohms of the load connected to Output 1
+- `Output 2 Resistance`: the resistance value in Ohms of the load connected to Output 2
+
+Be careful to put a value that you have correctly measured with a multimeter, or calculated (see formula below).
+An approximation will cause the router to not properly work because it won't be able to adjust the exact amount of power to send.
+
+Remember the equation:
+
+```
+R = U * U / P
+```
+
+where:
+
+- `P` is the _nominal_ power in Watts given by the manufacturer of the resistance
+- `U` is the _nominal_ voltage in Volts, usually 230V in Europe and 120V in the US/Canada
+- `R` is the resistance in Ohms
+
+**Reading the resistance value from a PZEM or JSY**
+
+If you have a PZEM or JSY device attached, they can help you.
+You can set the dimmer in manual mode and set it to 50% and 100% and read the resistance value.
+Then you just have to report it in the `Hardware Config` page.
+
+- **PZEM-004T v3:** If you have wired a PZEM-004T v3 connected to each output, it will measure the resistance value when routing.
+- **JSY-MK-194T:** If you have a JSY-MK-194T, you can activate the dimmers one by one to 100% and wait for the values to stabilize.
+ The router will then display the resistance value in the `Overview` page, thanks to the JSY.
+
+**Using the automatic detection feature** (🚧)
+
+- `Output 1 Resistance Detection`: start the automatic detection of the resistance value of the load connected to Output 1
+- `Output 2 Resistance Detection`: start the automatic detection of the resistance value of the load connected to Output 2
+
+This is the easiest way to calibrate the resistance values: when a PZEM or JSY is installed, the router will be able to automatically calculate the resistance.
+For that, click on the corresponding buttons and wait a few seconds.
+You can at the same time check the statistics on the `Output` or `Overview` sections.
+Once done, the resistance value will be put in the corresponding field.
+Any previously set value will be erased.
+
+To use this feature, make sure that the resistance will really draw some current.
+It won't work if the water heater has already reached its threshold temperature.
+
+#### PZEM Pairing
+
+- `Output 1 PZEM Pairing`: starts the pairing procedure for Output 1 PZEM-004T v3 at address 0x01.
+- `Output 2 PZEM Pairing`: starts the pairing procedure for Output 2 PZEM-004T v3 at address 0x02.
+
+Each output supports the addition of a PZEM-004T v3 sensor to monitor the power sent to the resistive load specifically for this output.
+Thanks to the PZEM per output, it is also possible to get some more precise information like the dimmed RMS voltage, resistance value, etc.
+
+The PZEM-004T v3 devices has a special installation mode: you can install 2x PZEM-004T v3 devices on the same Serial TX/RX.
+To communicate with the right one, each output will use a different slave address.
+The initial setup requires to pair each PZEM-004T v3 with the corresponding output.
+
+1. Connect the 2x PZEM-004T v3 devices to the grid (L/N) and install the clamp around the wire at the exit of the dimmer of first output
+2. Only connect the terminal wire (+5V, GND, RX, TX) of the first PZEM-004T v3 to pair to Output 1
+3. Boot the device and open the console (`http://yasolr.local/console`)
+4. Got to the `Hardware` section to activate `Output 1 PZEM`.
+ It should be yellow if it has no electricity or if it is not paired.
+5. Press the `Output 1 PZEM Pairing` button.
+6. Verify that the pairing is successful in the console.
+ `Output 1 PZEM` should also be green in the `Hardware` section.
+7. **Disconnect this first PZEM-004T v3 from the ESP32**
+8. Connect the second PZEM (which has its clamp at the exit of the dimmer of the second output) to the ESP32
+9. Press the `Output 2 PZEM Pairing` button.
+10. Verify that the pairing is successful in the console.
+ `Output 2 PZEM` should also be green in the `Hardware` section.
+11. Now that the 2 devices have an address, you can connect them all to the ESP32
+
+You can verify that the pairing is successful by trying to activate the dimmer in the overview page, and see if you get the output power.
+
+This complex pairing procedure is not specific to this router project but is common to any PZEM-004T device when using several PZEM-004T v3 devices on the same Serial TX/RX.
+You can read more at:
+
+- [mathieucarbou/MycilaPZEM004Tv3](https://github.com/mathieucarbou/MycilaPZEM004Tv3)
+- [mandulaj/PZEM-004T-v30](https://github.com/mandulaj/PZEM-004T-v30)
+
+### `PID Controller` section
+
+> ##### DANGER
+> For advanced users only.
+{: .block-danger }
+
+This page allows to tune the PID algorithm used to control the automatic routing.
+Use only if you know what you are doing and know how to tweak a PID controller.
+
+You can change the PID settings at runtime and the effect will appear immediately.
+
+> ##### TIP
+> If you find better settings, please do not hesitate to share them with the community.
+{: .block-tip }
+
+[![](assets/img/screenshots/pid_tuning.jpeg)](assets/img/screenshots/pid_tuning.jpeg)
+
+- `Real-time PID Data`: can be activated to see the PID action in real time in teh graphs.
+- `Chart Reset`: click to reset the charts (has no effect on the PID controller).
+
+> ##### IMPORTANT
+>
+> - Do not leave `Real-time PID Data` option always activated because the data flow is so high that it impacts the ESP32 performance.
+>
+> - you are supposed to know how to tune a PID controller. If not, please research on Google.
+{: .block-important }
+
+Here are some basic links to start with, which talks about the code used under the hood:
+
+- [Improving the Beginner’s PID – Introduction](http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/)
+- [Improving the Beginner’s PID – Derivative Kick](http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-derivative-kick/)
+- [Introducing Proportional On Measurement](http://brettbeauregard.com/blog/2017/06/introducing-proportional-on-measurement/)
+- [Proportional on Measurement – The Code](http://brettbeauregard.com/blog/2017/06/proportional-on-measurement-the-code/)
+
+**Default Settings**
+
+- `Proportional Mode`: `On Input`
+- `Derivative Mode`: `On Error`
+- `Integral Correction`: `Advanced`
+- `Setpoint`: `0`
+- `Kp`: `0.3`
+- `Ki`: `0.3`
+- `Kd`: `0.1`
+- `Output Min`: `-10000`
+- `Output Max`: `10000`
+
+**PID Tuning through WebSocket**
+
+When `Real-time PID Data` is activated, a WebSocket endpoint is available at `/ws/pid/csv` and will stream all the PID data in real time in a `CSV` format when automatic dimmer control is activated.
+You can quickly show then and process then in `bash` with `websocat` by typing for example:
+
+```bash
+❯ websocat ws://192.168.125.123/ws/pid/csv
+pMode,dMode,icMode,rev,setpoint,kp,ki,kd,outMin,outMax,input,output,error,sum,pTerm,iTerm,dTerm
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,780.645,1109.700,19.355,1104.889,7.217,7.742,4.811
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,778.620,1114.453,21.380,1114.048,0.607,8.552,0.405
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,774.128,1126.643,25.872,1125.745,1.347,10.349,0.898
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,786.127,1125.294,13.873,1127.694,-3.600,5.549,-2.400
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,804.696,1116.531,-4.696,1120.245,-5.571,-1.878,-3.714
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,820.285,1104.337,-20.285,1107.455,-4.677,-8.114,-3.118
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,822.300,1097.527,-22.300,1097.930,-0.605,-8.920,-0.403
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,808.928,1101.045,-8.928,1098.370,4.012,-3.571,2.674
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,798.264,1104.396,1.736,1102.264,3.199,0.694,2.133
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,793.393,1107.342,6.607,1106.368,1.461,2.643,0.974
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,785.225,1116.361,14.775,1114.728,2.450,5.910,1.634
+2,1,2,0,800,0.300,0.400,0.200,-10000,10000,821.839,1087.686,-21.839,1095.008,-10.984,-8.736,-7.323
+```
+
+You can also stream this data directly to a command-line tool that will plot in real time the graphs.
+Example of such tools:
+
+- https://github.com/keithknott26/datadash
+- https://github.com/cactusdynamics/wesplot
+
+**Demo**
+
+Here is a demo of the real-time PID tuning in action:
+
+[![PID Tuning in YaSolR (Yet Another Solar Router)](https://img.youtube.com/vi/ygSpUxKYlUE/0.jpg)](https://www.youtube.com/watch?v=ygSpUxKYlUE "PID Tuning in YaSolR (Yet Another Solar Router)")
+
+### `Statistics` section
+
+This page shows a lot of statistics and information on the router.
+
+[![](assets/img/screenshots/statistics.jpeg)](assets/img/screenshots/statistics.jpeg)
+
+## Important Hardware Information
+
+### Bypass Relay
+
+Installing a relay for bypass is optional: if installed, the relay will be used to power the heater, and the dimmer will be set to 0.
+
+If not installed, when activating bypass mode, the dimmer will be used and set to 100%.
+The advantage is a simple setup, the drawbacks are:
+
+- the dimmer will heat up.
+- the power output of he dimmer counts as routed power so the routed power and energy will also contain the bypass power.
+
+In the `Hardware` section, `Output 1 Relay (Bypass)` and `Output 2 Relay (Bypass)` both specify if a relay is installed for the output, on top of the dimmer of course, and if it should be used when bypass is activated.
+If no relay is installed, the dimmer will be used and will be set to 100%.
+
+In the `Hardware Config` section, `Output 1 Relay Type (Bypass)` and `Output 2 Relay Type (Bypass)` are used to specify the type of the relay: `Normally Open` or `Normally Closed`.
+
+### Display
+
+Supported displays are any I2C OLED Display of type `SSD1307`, `SH1106`, `SH1107`.
+
+`SH1106` is recommended and has been extensively tested.
+
+[![](assets/img/screenshots/display.gif)](assets/img/screenshots/display.gif)
+
+The display will look like a carousel with a maximum of 5 pages:
+
+- Global information
+- Network information
+- Router information with relays
+- Output 1 information
+- Output 2 information
+
+### JSY-MK-194T (local)
+
+The JSY is used to measure:
+
+1. the total routed power of the outputs combined (optional) with the channel 1 (tore or clamp depending on the model)
+2. the grid power and voltage with the clamp of channel 2
+
+The JSY can be replaced by MQTT, reading the power and voltage from MQTT topics.
+See [MQTT as a Grid Source](#mqtt-as-a-grid-source).
+
+### JSY-MK-194T (remote)
+
+JSY can also be replaced with a remote JSY **without any impact on routing speed**.
+You can install the [Sender](https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP) .ino file on a ESP32 and connect it to the JSY.
+This is a standalone application that looks looks like this and will show all your JSY data, help you manage it, and also send the data through UDP **at a rate of 20 messages per second**.
+
+![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0)
+
+You can look in the [JSY project](https://oss.carbou.me/MycilaJSY/) to find more information about how to setup remote JSY and the supported protocols.
+
+When using a remote JSY with the router, the following rules apply:
+
+- The voltage will always be read if possible from a connected JSY or PZEM, then from a remote JSY, then from MQTT.
+- The grid power will always be read first from MQTT, then from a remote JSY, then from a connected JSY.
+
+> ##### TIP
+> JSY Remote app is automatically detected on the same network: you don't need to configure anything.
+> As soon as the Sender app will start sending data, YaSolR will receive it and display it.
+{: .block-tip }
+
+### LEDs
+
+The LEDs are used to notify the user of some events like reset, restarts, router ready, routing, etc.
+
+| **LIGHTS** | **SOUNDS** | **STATES** |
+| :--------: | ---------------- | ------------------------------- |
+| `🟢 🟡 🔴` | `BEEP BEEP` | `STARTED` + `POWER` + `OFFLINE` |
+| `🟢 🟡 ⚫` | | `STARTED` + `POWER` |
+| `🟢 ⚫ 🔴` | `BEEP BEEP` | `STARTED` + `OFFLINE` |
+| `🟢 ⚫ ⚫` | `BEEP` | `STARTED` |
+| `⚫ 🟡 🔴` | `BEEP BEEP BEEP` | `RESET` |
+| `⚫ 🟡 ⚫` | | |
+| `âš« âš« đź”´` | `BEEP BEEP` | `RESTART` |
+| `âš« âš« âš«` | | `OFF` |
+
+- `STARTED`: application started and WiFi or AP mode connected
+- `OFFLINE`: application disconnected from WiFi or disconnected from grid electricity
+- `POWER`: power allowed to be sent (either through relays or dimmer)
+- `RESTART`: application is restarting following a manual restart
+- `RESET`: application is restarting following a manual reset
+- `OFF`: application not working (power off)
+
+### Temperature Sensor
+
+The temperature sensors are used to monitor the water tank in order:
+
+- to trigger an automatic heating based on temperature levels (called **auto bypass**).
+- to stop the routing if the temperature is too high (called **temperature limiter**).
+
+Supported temperature sensor: `DS18B20`
+
+A temperature sensor can also be used to monitor the router box itself (`Overview` section).
+
+### Zero-Cross Detection
+
+The Zero-Cross Detection (ZCD) module is used to detect the zero-crossing of the grid voltage.
+It is required, whether you use a Robodyn or SSR or any routing algorithm (phase control or burst mode).
+
+> ##### TIP
+> The Robodyn includes a ZCD (its ZC pin).
+> Do not forget to activate the ZCD module in the `Hardware` section.
+{: .block-tip }
+
+You can also use a dedicated ZCD module like the one suggested on this website (build menu).
+
+### Virtual Grid Power / Compatibility with EV box
+
+The router exposes through API and MQTT the **Virtual Grid Power**, which is the value of Grid Power you would have if the router was not routing.
+
+You can use this value to inject in the EV box in order to prioritize EV charging over routing to the water tank.
+
+This is usually acceptable to give the EV box a priority over the water tank, because the water tank only need a small amount of routed energy to start heating, while the EV usually requires a threshold to start charging.
+So the router will take whatever is not used by the EV box.
+
+> ##### IMPORTANT
+>
+> `Virtual Grid Power` requires a PZEM or JSY in place to measure the routed power.
+{: .block-important }
+
+## Help and support
+
+- **Facebook Group**: [https://www.facebook.com/groups/yasolr](https://www.facebook.com/groups/yasolr)
+
+- **GitHub Discussions**: [https://github.com/mathieucarbou/YaSolR-OSS/discussions](https://github.com/mathieucarbou/YaSolR-OSS/discussions)
+
+- **GitHub Issues**: [https://github.com/mathieucarbou/YaSolR-OSS/issues](https://github.com/mathieucarbou/YaSolR-OSS/issues)
+
+- **ESP32 Exception Decoder**: [https://maximeborges.github.io/esp-stacktrace-decoder/](https://maximeborges.github.io/esp-stacktrace-decoder/)
diff --git a/docs/mqtt.md b/docs/mqtt.md
new file mode 100644
index 0000000..b3b4a58
--- /dev/null
+++ b/docs/mqtt.md
@@ -0,0 +1,276 @@
+---
+layout: default
+title: MQTT API
+description: MQTT API
+---
+
+# MQTT Topics
+
+- [`/status`](#status)
+- [`/config`](#config)
+- [`/grid`](#grid)
+- [`/router`](#router)
+- [`/router/outputX`](#routeroutputx)
+- [`/router/outputX/dimmer`](#routeroutputxdimmer)
+- [`/router/relayX`](#routerrelayx)
+- [`/system/app`](#systemapp)
+- [`/system/device`](#systemdevice)
+- [`/system/device/restart`](#systemdevicerestart)
+- [`/system/device/heap`](#systemdeviceheap)
+- [`/system/firmware`](#systemfirmware)
+- [`/system/firmware/build`](#systemfirmwarebuild)
+- [`/system/network`](#systemnetwork)
+- [`/system/network/eth`](#systemnetworketh)
+- [`/system/network/wifi`](#systemnetworkwifi)
+
+Not everything MQTT topic will update frequently (5 sec by default).
+Some topics, like configuration related, will only update when the configuration is changed.
+These will have the retain flag set to true so that a subscriber coming after the data was published will still get the update.
+
+## `/status`
+
+This is the will topic which can be used to detect when the device is connected or gone.
+It is also used by Home Assistant discovery.
+It is set to `online` or `offline`
+
+## `/config`
+
+```properties
+admin_pwd = ********
+ap_mode_enable = false
+debug_enable = true
+disp_angle = 0
+disp_enable = true
+disp_speed = 3
+disp_type = SH1106
+ds18_sys_enable = true
+grid_freq = 50
+grid_pow_mqtt = homeassistant/states/sensor/grid_power/state
+grid_volt = 230
+grid_volt_mqtt =
+ha_disco_enable = true
+ha_disco_topic = homeassistant/discovery
+jsy_enable = true
+lights_enable = true
+mqtt_enable = true
+mqtt_port = 1883
+mqtt_pub_itvl = 5
+mqtt_pwd = ********
+mqtt_secure = false
+mqtt_server = 192.168.125.90
+mqtt_topic = yasolr_a1c48
+mqtt_user = homeassistant
+ntp_server = pool.ntp.org
+ntp_timezone = Europe/Paris
+o1_ab_enable = false
+o1_ad_enable = false
+o1_days = sun,mon,tue,wed,thu,fri,sat
+o1_dim_enable = true
+o1_dim_limit = 100
+o1_ds18_enable = true
+o1_pzem_enable = true
+o1_relay_enable = true
+o1_relay_type = NO
+o1_temp_start = 50
+o1_temp_stop = 60
+o1_time_start = 22:00
+o1_time_stop = 06:00
+o2_ab_enable = false
+o2_ad_enable = false
+o2_days = sun,mon,tue,wed,thu,fri,sat
+o2_dim_enable = true
+o2_dim_limit = 27
+o2_ds18_enable = true
+o2_pzem_enable = true
+o2_relay_enable = false
+o2_relay_type = NO
+o2_temp_start = 50
+o2_temp_stop = 60
+o2_time_start = 22:00
+o2_time_stop = 06:00
+pin_disp_scl = 22
+pin_disp_sda = 21
+pin_ds18 = 4
+pin_jsy_rx = 16
+pin_jsy_tx = 17
+pin_lights_g = 0
+pin_lights_r = 15
+pin_lights_y = 2
+pin_o1_dim = 25
+pin_o1_ds18 = 18
+pin_o1_relay = 32
+pin_o2_dim = 26
+pin_o2_ds18 = 5
+pin_o2_relay = 33
+pin_pzem_rx = 14
+pin_pzem_tx = 27
+pin_relay1 = 13
+pin_relay2 = 12
+pin_zcd = 35
+relay1_enable = true
+relay1_load = 0
+relay1_type = NO
+relay2_enable = true
+relay2_load = 0
+relay2_type = NO
+wifi_pwd =
+wifi_ssid = IoT
+zcd_enable = true
+```
+
+**Update**
+
+```properties
+# Set a configuration key to a new value
+/config//set
+```
+
+## `/grid`
+
+```properties
+apparent_power = 0
+current = 0
+energy = 0
+energy_returned = 0
+frequency = 49.97999954
+online = true
+power = 617.6273804
+power_factor = 0
+voltage = 234.0296021
+```
+
+## `/router`
+
+```properties
+apparent_power = 0
+current = 0
+energy = 0.067999996
+lights = 🟢 🟡 ⚫
+power = 239.4906921
+power_factor = 0.495258361
+temperature = 26.30999947
+thdi = 1.550398946
+virtual_grid_power = -503.7492371
+```
+
+## `/router/outputX`
+
+```properties
+bypass = on
+state = Idle
+temperature = 26.3
+```
+
+**Update**
+
+```properties
+# Switch bypass on or off
+/router/outputX/bypass/set = "on"
+/router/outputX/bypass/set = "off"
+```
+
+## `/router/outputX/dimmer`
+
+```properties
+duty = 1000
+duty_cycle = 100
+state = on
+```
+
+**Update**
+
+```properties
+# Update the dimmer duty / duty cycle
+/router/outputX/dimmer/duty/set = [0, 4095]
+/router/outputX/dimmer/duty_cycle/set = [0.0, 100.0]
+```
+
+## `/router/relayX`
+
+```properties
+state = off
+```
+
+**Update**
+
+```properties
+# Switch relay on or off or for a duration in milliseconds
+/router/relayX/state/set = "on"
+/router/relayX/state/set = "on=5000"
+/router/relayX/state/set = "off"
+```
+
+## `/system/app`
+
+```properties
+manufacturer = Mathieu Carbou
+model = Pro
+name = YaSolR
+version = main_0ed7d852_modified
+```
+
+## `/system/device`
+
+```properties
+boots = 1
+cores = 2
+cpu_freq = 240
+id = A1C48
+model = ESP32-D0WD-V3
+uptime = 1675
+```
+
+## `/system/device/restart`
+
+Restarts tge device
+
+## `/system/device/heap`
+
+```properties
+total = 269404
+usage = 47.63
+used = 128316
+```
+
+## `/system/firmware`
+
+```properties
+debug = true
+filename = YaSolR-main-pro-esp32-debug.bin
+```
+
+## `/system/firmware/build`
+
+```properties
+branch = main
+hash = da49a3a
+timestamp = 2024-06-08T12:14:30.915965+00:00
+```
+
+## `/system/network`
+
+```properties
+hostname = yasolr-a1c48
+ip_address = 192.168.125.121
+mac_address = B0:B2:1C:0A:1C:48
+mode = wifi
+ntp = on
+```
+
+## `/system/network/eth`
+
+```properties
+ip_address = 0.0.0.0
+mac_address = B0:B2:1C:0A:1C:50
+```
+
+## `/system/network/wifi`
+
+```properties
+bssid = 00:17:13:37:28:C0
+ip_address = 0.0.0.0
+mac_address = B0:B2:1C:0A:1C:50
+quality = 100
+rssi = -21
+ssid = IoT
+```
diff --git a/docs/overview.md b/docs/overview.md
new file mode 100644
index 0000000..f4ffd27
--- /dev/null
+++ b/docs/overview.md
@@ -0,0 +1,229 @@
+---
+layout: default
+title: Overview
+description: Overview
+---
+
+# Overview
+
+- [What is a Solar Router ?](#what-is-a-solar-router-)
+- [How a Solar Router work ?](#how-a-solar-router-work-)
+ - [Zero-Cross Detection (ZCD)](#zero-cross-detection-zcd)
+ - [Robodyn and Solid State Relay (SSR)](#robodyn-and-solid-state-relay-ssr)
+- [Phase Control](#phase-control)
+ - [Harmonics](#harmonics)
+- [Burst Fire Control](#burst-fire-control)
+ - [Flickering](#flickering)
+- [Recommendations to reduce harmonics and flickering](#recommendations-to-reduce-harmonics-and-flickering)
+- [References](#references)
+
+## What is a Solar Router ?
+
+A _Solar Router_ allows to redirect the solar production excess to some appliances instead of returning it to the grid.
+The particularity of a solar router is that it will dim the voltage and power sent to the appliance in order to match the excess production, in contrary to a simple relay that would just switch on/off the appliance without controlling its power.
+
+A _Solar Router_ is usually connected to the resistance of a water tank and will heat the water when there is production excess.
+
+A solar router can also do more things, like controlling (on/off) the activation of other appliances (with the grid normal voltage and not the dimmed voltage) in case the excess reaches a threshold. For example, one could activate a pump, pool heater, etc if the excess goes above a specific amount, so that this appliance gets the priority over heating the water tank.
+
+A router can also schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage. This is called a bypass relay.
+
+## How a Solar Router work ?
+
+A router is composed of 2 main pieces:
+
+1. **A bi-directional measurement system** that will detect the solar production excess and the home consumption in Watts: Linky TIC, Shelly EM, JSY, etc
+
+2. A **dimmer system** that will control the voltage and power sent to the resistance of the water tank to match the measured excess: Robodyn AC Dimmer, Random Solid State Relay, etc
+
+The dimmer systems are usually based on TRIAC / Thyristors and are controlling the power through different methods:
+
+| **Phase Control** | **Burst Fire Control** |
+| :-----------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| ![](assets/img/measurements/Oscillo_Dimmer_50.jpeg) | ![](assets/img/measurements/Burst_50.png) |
+| In this mode, the TRIAC lets the current pass at a specific moment within the 2 semi-periods of 10 ms by "cutting" the sin wave | In this mode, the TRIAC is used like a rapid switch, to let pass a sequence of complete full periods or semi-periods without cutting them |
+| **Used devices:**
Robodyn, Random SSR | **Used devices:**
Robodyn, Random SSR, Zero-Cross SSR |
+| **Pros:**
More precise routing, can control exactly the right amount of power to let pass | **Pros:**
Easier to grasp and implement and does not create harmonics |
+| **Cons:**
Can cause harmonics that can be difficult to filter out (but effect can be limited or mitigated) | **Cons:**
Less precise routing because each complete period (or semi-period) lets the full power (or half power) pass, and can cause flickering (light bulbs that are close-by can blink because of the fast and successive current switches) |
+
+Other algorithms exist, more or less complex but generally based on these 2 methods.
+
+### Zero-Cross Detection (ZCD)
+
+To know when to switch or cut the voltage wave, routers are using a **Zero-Cross Detection (ZCD) circuit** that will detect when the voltage curve crosses the Zero point (which is twice per period) and will send a pulse to the controller board.
+Here are below some examples of how a ZCD circuit works by looking at 2 different implementations: Robodyn and a more specific one from [Daniel S.](https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html).
+
+| **Dedicated ZCD circuit** | **Robodyn ZCD circuit** |
+| :------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------: |
+| [![ZCD](assets/img/measurements/Oscillo_ZCD.jpeg)](assets/img/measurements/Oscillo_ZCD.jpeg) | [![ZCD](assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg)](assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg) |
+
+When the AC voltage curve crosses the Zero point, the ZCD circuit sends a pulse (with a custom duration) to the controller board, which now knows that the voltage is at zero.
+The board then does some calculation to determine when to send the signal to the TRIAC (or Random SSR or Robodyn) to activate it, based on the excess power, or if using burst fire control, to know when to let the current pass and for how many semi-periods.
+
+### Robodyn and Solid State Relay (SSR)
+
+A Solid State Relay is a relay that does not have any moving parts and is based on a semiconductor.
+It can be turned on and off very fast.
+
+A **Zero-Cross SSR** is a relay that will only close or open when the voltage curve is at 0.
+It won't generate any harmonics and is not able to do Phase Control, but it can be used for Burst Fire Control.
+
+A **Random** SSR is a relay that can be turned on and off at any point in time, at any voltage level.
+It can be used for Phase Control and Burst Fire Control.
+If activated when the voltage curve is not at 0, it will generate harmonics.
+
+Due to the nature of SSR, the more they are used (switched on/off), the more they will heat up.
+So it is recommended to install them on a vertical heat sink.
+
+SSR also have some specifications to take into account for the use of a Solar Router:
+
+- **Type of control**: DA: (DC Control AC)
+- **Control voltage**: 3.3V should be in the range (example: 3-32V DC)
+
+**Robodyn** is a device that includes both a ZCD circuit and a TRIAC, which makes it ideal for a Solar Router using Phase Control System.
+Using a Random SSR instead of the Robodyn is possible but will require the use of an additional ZCD circuit.
+
+## Phase Control
+
+**Effect on current**
+
+In this mode, the TRIAC lets the current pass at a specific moment within the 2 semi-periods of 10 ms by "cutting" the sin wave.
+
+Here are 3 different views from an Owon VDS6104 oscilloscope of:
+
+1. The AC voltage at dimmer input (red)
+2. The ZCD pulse detected by the ESP32 board when the voltage crosses the Zero line (yellow)
+3. The control voltage of the random SSR (or Robodyn) that is sent from the ESP32 board. The TRIAC will let the current pass when the control voltage is >= 3.3V (blue)
+4. The dimmed AC current at dimmer output (pink)
+
+| | **Dimmer Duty 10 / 4095** | **Dimmer Duty 2047 / 4095 (50%)** | **Dimmer Duty 4090 / 4095** |
+| :-------------------: | :---------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------: |
+| **Robodyn** | [![](assets/img/measurements/Robodyn_duty_10.png)](assets/img/measurements/Robodyn_duty_10.png) | [![](assets/img/measurements/Robodyn_duty_2047.png)](assets/img/measurements/Robodyn_duty_2047.png) | [![](assets/img/measurements/Robodyn_duty_4090.png)](assets/img/measurements/Robodyn_duty_4090.png) |
+| **Better ZCD Module** | [![](assets/img/measurements/ZCD_duty_10.png)](assets/img/measurements/ZCD_duty_10.png) | [![](assets/img/measurements/ZCD_duty_2047.png)](assets/img/measurements/ZCD_duty_2047.png) | [![](assets/img/measurements/ZCD_duty_4090.png)](assets/img/measurements/ZCD_duty_4090.png) |
+
+Dimmer at 50% matches the 90 degrees angle of the voltage curve, so the current is chopped at 50% of the period. This is when the harmonic level is the highest.
+We can clearly see the effect of the TRIAC on the voltage curve, and the resulting current curve, which is chopped at the wanted level.
+
+Robodyn as a poor ZCD signal.
+If you can, take a better ZCD module.
+This will also help ZCD edge detection because the ESP32 is subject to [spurious interrupt issue](https://github.com/fabianoriccardi/dimmable-light/wiki/Notes-about-specific-architectures#interrupt-issue) when detecting ZCD edges.
+Hopefully this can be overcome by filtering out the noise in the code.
+
+**Effect on voltage**
+
+Now, let's see what is happening to the input and output voltage when the dimmer is at 20% with a current of about 1.7A.
+Measurements are done with the Owon HDS2202S: voltage in yellow and current in blue.
+
+| **At Router Input** | **At Router Output** |
+| :--------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------: |
+| [![](assets/img/measurements/Oscillo_Dim_20_In.jpeg)](assets/img/measurements/Oscillo_Dim_20_In.jpeg) | [![](assets/img/measurements/Oscillo_Dim_20_Out.jpeg)](assets/img/measurements/Oscillo_Dim_20_Out.jpeg) |
+| At Router input, before the dimmer, the voltage curve is normal. But the requested current shape is as defined by the TRIAC activations. | At Router output, after the dimmer, both the voltage and current curves are chopped according to the TRIAC activation |
+
+### Harmonics
+
+The biggest issue with Phase Control is that consecutively chopping the voltage curve creates some spontaneous current request especially at the point when the voltage at its minimum or maximum.
+This could be compared to suddenly opening a water valve instead of gradually opening it. The pressure is higher and the water flow will be more turbulent.
+These harmonics are bigger when the TRIAC lets the current pass at 90 degrees (50% of the nominal power).
+
+Harmonics are causing a distortion of the voltage and current curves, and are transporting some energy at higher frequencies, multiples of the fundamental frequency (50Hz in Europe, 60Hz in the US).
+The power factor is also impacted by harmonics: the more harmonics there are, the more the power factor will be degraded.
+
+![](assets/img/measurements/Harmoniques.jpeg)
+
+Harmonics are not bad, but they can be damaging for some appliances if they are too high, such as motors, UPS, electronic devices, etc.
+Harmonics are also regulated according to [CEI 61000-3-2](http://crochet.david.online.fr/bep/copie%20serveur/Normes/cei%2061000-3-2.pdf) (A Solar Router is a Class A device).
+This document gives the maximum current allowed for each harmonic level.
+
+[![](assets/img/measurements/CEI%2061000-3-2.png)](assets/img/measurements/CEI%2061000-3-2.png)
+
+Some studies were done to determine the level of harmonics a Solar Router would generate and at which power. Here are some key things to consider:
+
+- The worst case scenario is when the TRIAC angle is at 90 degrees (50% power)
+- The Harmonic #15 is the first harmonic level to be reached with a nominal load of about **760 W**
+- But H15 is insignificant compared to H3, which is the most significant one in terms of energy transported
+- The Harmonic #3 maximum level according to CEI 61000-3-2 is reached with a nominal load of about **1700 W**
+- To stay compliant with CEI 61000-3-2, the maximum nominal load should be less than 800 W
+
+| **Harmonic #3** | **Harmonic #15** |
+| :-------------------------------------------------------------------: | :---------------------------------------------------------------------: |
+| [![](assets/img/measurements/H3.png)](assets/img/measurements/H3.png) | [![](assets/img/measurements/H15.png)](assets/img/measurements/H15.png) |
+
+To put things in perspective, it is important to remember that a Solar Router will adapt the TRIAC angle based on the excess power, so **the router will not always be dimming at 90 degrees**, at the worst case scenario.
+
+## Burst Fire Control
+
+Burst Fire Control will let a complete or half complete voltage curve pass or not, and this control is done from the zero point up to the next.
+So the sin wave is not chopped like in Phase Control, but we decide to let pass or not a complete period or half period.
+
+50Hz current has a voltage curve with a period of 20 ms decoupled in 2 half-periods: one positive, one negative, so the zero voltage is crossed twice per period.
+A measuring device like JSY makes about 300 ms to see a new value, which gives us 15 full periods and 30 half-periods to re-arrange the current flow, before the next measurement.
+
+This method can use a simple Zero-Cross Solid State Relay: a relay that will only close or open when the voltage curve is at 0.
+So there is no load at that time of switching, thus, no harmonics.
+
+**This method is not as accurate as Phase Control, but still provides good results, depending on the load.**
+
+### Flickering
+
+The main problem with Burst Fire Control is that some kind of arrangements can cause flickering when the nominal load is big (big power tanks), visible on light bulbs that are close-by.
+This is caused by a sudden voltage drop in the house, caused by a sudden current flow at the request of the big water tank resistance.
+
+## Recommendations to reduce harmonics and flickering
+
+Harmonics and flickering cannot be completely avoided but they can be mitigated or limited by following some recommendations:
+
+1. YaSolR has a "limiter" to prevent the router from dimming more than the limit set.
+ For example, if you have a nominal load of 3000W and set the limiter to 50%, the router will not use more than 1500W of excess.
+
+2. Reduce cable length between the router and the resistance to the bare minimum (a few meters) and use wider cables
+
+3. Remove any sensitive equipment close to wires going in the router and out of the router
+
+4. Put your router and resistance circuit as close as possible to the grid entrance and exit (eventually by re-arranging the DIN rail)
+
+5. Change your water tank resistance to a resistance with less nominal load (more ohms) in order to decrease the current load.
+ For example, a 3000W resistance with 18 ohms will have a current load of 7.4A when dimmed at 134V for 1000W of excess and will generate more harmonics or will cause more flickering.
+ While a 1000W resistance with 53 ohms will have a current load of only 4.3A and will be used at full power.
+
+6. Switch your water tank resistance with a a tri-phase resistance in order to be able to control 3 resistances in steps independently: they will be activated step by step. Example:
+
+ - Resistance 1: 800 W connected to the dimmer: the dimmer will route the solar excess from 0 to 800 W to this resistance
+ - Resistance 2: 800 W connected to relay 1: when the excess is above 800 W, the relay will activate (all or nothing relay) and the second resistance will receive 800 W of excess.
+ The first resistance connected to the Phase Control system (dimmer or Random SSR) will receive what is remaining from the excess.
+ - Resistance 3: 800 W connected to relay 2: will work the same and will be activated when the excess will be above 800W.
+
+7. If some devices are impacted by harmonics (such as an EV box restarting), try put an RC Snubber at your dimmer output between phase (going to the load) and neutral.
+ It won't solve the harmonic issue but can help mitigate equipments sensible to energy spikes.
+ Suggestions often seen are to use a 100 ohms 0.1uF (100nF) RC Snubber (like the ones sold for Shelly devices).
+
+## References
+
+**Solar Routers**
+
+- [Avantage d'un routeur solaire](https://sites.google.com/view/le-professolaire/routeur-professolaire) (Anthony G., _Le Profes'Solaire_)
+- [Principe du routeur photovoltaïque](https://f1atb.fr/fr/realisation-dun-routeur-photovoltaique-multi-sources-multi-modes-et-modulaire/) (André B., _[F1ATB](https://github.com/F1ATB)_)
+
+**YouTube videos** explaining the theory behind a Solar Router, harmonics and solutions, with some simulations and practical examples.
+
+- [Installations photovoltaĂŻques](https://www.youtube.com/playlist?list=PLWpzro3Ndk_2PUlQkULUjP6VSzwmFXkPc) (Pierre Chfd)
+- [Routeur solaire ongrid](https://www.youtube.com/playlist?list=PL-IXE4AO5wkuxvQLEB-AuwoxZF1ZRzClf) (SĂ©bastien P., _[SeByDocKy](https://github.com/SeByDocKy)_)
+
+**Harmonics**
+
+- [Etude des harmoniques du courant de ligne](https://www.thierry-lequeu.fr/data/TRIAC.pdf) (Thierry Lequeu)
+- [Détection et atténuation des harmoniques](https://fr.electrical-installation.org/frwiki/Détection_et_atténuation_des_harmoniques) (Schneider Electric)
+- [Router via TRIAC et "Pollution" du réseau](https://forum-photovoltaique.fr/viewtopic.php?t=60521) (Forum photovoltaïque)
+- [HARMONICS: CAUSES, EFFECTS AND MINIMIZATION](https://www.salicru.com/files/pagina/72/278/jn004a01_whitepaper-armonics_%281%29.pdf) (Ramon Pinyol, R&D Product Leader SALICRU)
+- [HARMONIQUES ET DEPOLLUTION DU RESEAU ELECTRIQUE](http://archives.univ-biskra.dz/bitstream/123456789/21913/1/BELHADJ%20KHEIRA%20ET%20BOUZIR%20NESSRINE.pdf) (BELHADJ KHEIRA ET BOUZIR NESSRINE)
+- [Impact de la pollution harmonique sur les mateĚriels de reĚseau](https://theses.hal.science/tel-00441877/document) (Wilfried Frelin)
+
+**TRIAC**
+
+- [NEW TRIACS: IS THE SNUBBER CIRCUIT NECESSARY?](https://www.thierry-lequeu.fr/data/AN437.pdf) (Thierry Lequeu)
+- [Le triac](https://emrecmic.wordpress.com/2017/02/07/le-triac/)
+- [Le gradateur](http://philippe.demerliac.free.fr/RichardK/Graduateur.pdf) (Richard KOWAL)
+
+**Technical docs and algorithms**
+
+- [Learn: PV Diversion](https://docs.openenergymonitor.org/pv-diversion/)
+- [Optimized Random Integral Wave AC Control Algorithm for AC heaters](https://tsltd.github.io)
diff --git a/docs/rest.md b/docs/rest.md
new file mode 100644
index 0000000..9d51cba
--- /dev/null
+++ b/docs/rest.md
@@ -0,0 +1,380 @@
+---
+layout: default
+title: HTTP API
+description: HTTP API
+---
+
+# Web Endpoints
+
+- [`/api`](#api)
+- [`/api/config`](#apiconfig)
+- [`/api/config/backup`](#apiconfigbackup)
+- [`/api/grid`](#apigrid)
+- [`/api/router`](#apirouter)
+- [`/api/system`](#apisystem)
+- [`/api/system/reset`](#apisystemreset)
+- [`/api/restart`](#apirestart)
+
+## `/api`
+
+List all available endpoints
+
+```bash
+curl -X GET http:///api
+```
+
+```json
+{
+ "config": "http://192.168.125.123/api/config",
+ "config/backup": "http://192.168.125.123/api/config/backup",
+ "debug": "http://192.168.125.123/api/debug",
+ "grid": "http://192.168.125.123/api/grid",
+ "router": "http://192.168.125.123/api/router",
+ "system": "http://192.168.125.123/api/system",
+ "system/reset": "http://192.168.125.123/api/system/reset",
+ "system/restart": "http://192.168.125.123/api/system/restart"
+}
+```
+
+## `/api/config`
+
+Configuration view, update, backup and restore
+
+```bash
+curl -X GET http:///api/config
+```
+
+```json
+{
+ "admin_pwd": "",
+ "ap_mode_enable": "false",
+ "debug_enable": "true",
+ "disp_angle": "0",
+ "disp_enable": "true",
+ "disp_speed": "3",
+ "disp_type": "SH1106",
+ "ds18_sys_enable": "true",
+ "grid_freq": "50",
+ "grid_pow_mqtt": "",
+ "grid_volt_mqtt": "",
+ "ha_disco_enable": "true",
+ "ha_disco_topic": "homeassistant/discovery",
+ "jsy_enable": "true",
+ "lights_enable": "true",
+ "mqtt_enable": "true",
+ "mqtt_port": "1883",
+ "mqtt_pub_itvl": "5",
+ "mqtt_pwd": "********",
+ "mqtt_secure": "false",
+ "mqtt_server": "192.168.125.90",
+ "mqtt_topic": "yasolr_a1c48",
+ "mqtt_user": "homeassistant",
+ "ntp_server": "pool.ntp.org",
+ "ntp_timezone": "Europe/Paris",
+ "o1_ab_enable": "false",
+ "o1_ad_enable": "false",
+ "o1_days": "sun,mon,tue,wed,thu,fri,sat",
+ "o1_dim_enable": "true",
+ "o1_dim_max_d": "4095",
+ "o1_dim_max_t": "60",
+ "o1_ds18_enable": "true",
+ "o1_excess_ratio": "100",
+ "o1_pzem_enable": "true",
+ "o1_relay_enable": "true",
+ "o1_relay_type": "NO",
+ "o1_resistance": "24",
+ "o1_temp_start": "50",
+ "o1_temp_stop": "60",
+ "o1_time_start": "22:00",
+ "o1_time_stop": "06:00",
+ "o2_ab_enable": "false",
+ "o2_ad_enable": "false",
+ "o2_days": "sun,mon,tue,wed,thu,fri,sat",
+ "o2_dim_enable": "true",
+ "o2_dim_max_d": "4095",
+ "o2_dim_max_t": "60",
+ "o2_ds18_enable": "true",
+ "o2_excess_ratio": "100",
+ "o2_pzem_enable": "true",
+ "o2_relay_enable": "true",
+ "o2_relay_type": "NO",
+ "o2_resistance": "85",
+ "o2_temp_start": "50",
+ "o2_temp_stop": "60",
+ "o2_time_start": "22:00",
+ "o2_time_stop": "06:00",
+ "pid_dmode": "1",
+ "pid_icmode": "2",
+ "pid_kd": "0.1",
+ "pid_ki": "0.3",
+ "pid_kp": "0.3",
+ "pid_out_max": "10000",
+ "pid_out_min": "-10000",
+ "pid_pmode": "2",
+ "pid_setpoint": "0",
+ "pid_view_enable": "false",
+ "pin_disp_scl": "22",
+ "pin_disp_sda": "21",
+ "pin_ds18": "4",
+ "pin_jsy_rx": "16",
+ "pin_jsy_tx": "17",
+ "pin_lights_g": "0",
+ "pin_lights_r": "15",
+ "pin_lights_y": "2",
+ "pin_o1_dim": "25",
+ "pin_o1_ds18": "18",
+ "pin_o1_relay": "32",
+ "pin_o2_dim": "26",
+ "pin_o2_ds18": "5",
+ "pin_o2_relay": "33",
+ "pin_pzem_rx": "14",
+ "pin_pzem_tx": "27",
+ "pin_relay1": "13",
+ "pin_relay2": "12",
+ "pin_zcd": "35",
+ "relay1_enable": "true",
+ "relay1_load": "0",
+ "relay1_type": "NO",
+ "relay2_enable": "true",
+ "relay2_load": "0",
+ "relay2_type": "NO",
+ "udp_port": "53964",
+ "wifi_pwd": "",
+ "wifi_ssid": "IoT",
+ "zcd_enable": "true"
+}
+```
+
+```bash
+# Configuration Update:
+curl -X POST \
+ -F "hostname=foobarbaz" \
+ -F "admin_password=" \
+ -F "ntp_server=fr.pool.ntp.org" \
+ -F "ntp_timezone=Europe/Paris" \
+ [...]
+ http:///api/config
+```
+
+## `/api/config/backup`
+
+```bash
+# Backup configuration config.txt:
+curl -X GET http:///api/config/backup
+```
+
+```bash
+# Restore configuration config.txt:
+curl -X POST -F "data=@./path/to/config.txt" http:///api/config/restore
+```
+
+## `/api/grid`
+
+Display grid electricity information
+
+```bash
+curl -X GET http:///api/grid
+```
+
+```json
+{
+ "apparent_power": -782.8499756,
+ "current": 3.253999949,
+ "energy": 407.8739929,
+ "energy_returned": 21.01000023,
+ "frequency": 50.04000092,
+ "online": true,
+ "power": -10.9598999,
+ "power_factor": 0.014,
+ "voltage": 234.0771942
+}
+```
+
+## `/api/router`
+
+Show the router information and allows to control the relays, dimmers and bypass
+
+```bash
+curl -X GET http:///api/router
+```
+
+```json
+{
+ "lights": "🟢 ⚫ ⚫",
+ "temperature": 26.12999916,
+ "virtual_grid_power": 688.6345825,
+ "measurements": {
+ "apparent_power": 0,
+ "current": 0,
+ "energy": 0,
+ "power": 0,
+ "power_factor": 0,
+ "resistance": 0,
+ "thdi": 0,
+ "voltage": 237.3479004,
+ "voltage_dimmed": 0
+ },
+ "output1": {
+ "bypass": "off",
+ "state": "Idle",
+ "temperature": 24.62999916,
+ "dimmer": {
+ "duty": 0,
+ "duty_cycle": 0,
+ "state": "off"
+ },
+ "measurements": {
+ "apparent_power": 0,
+ "current": 0,
+ "energy": 0.388000011,
+ "power": 0,
+ "power_factor": 0,
+ "resistance": 0,
+ "thdi": 0,
+ "voltage": 237.3999939,
+ "voltage_dimmed": 0
+ },
+ "relay": {
+ "state": "off",
+ "switch_count": 0
+ }
+ },
+ "output2": {
+ "bypass": "off",
+ "state": "Idle",
+ "temperature": 24.62999916,
+ "dimmer": {
+ "duty": 0,
+ "duty_cycle": 0,
+ "state": "off"
+ },
+ "measurements": {
+ "apparent_power": 0,
+ "current": 0,
+ "energy": 0.647000015,
+ "power": 0,
+ "power_factor": 0,
+ "resistance": 0,
+ "thdi": 0,
+ "voltage": 237.6000061,
+ "voltage_dimmed": 0
+ },
+ "relay": {
+ "state": "off",
+ "switch_count": 0
+ }
+ },
+ "relay1": {
+ "state": "off",
+ "switch_count": 0
+ },
+ "relay2": {
+ "state": "off",
+ "switch_count": 0
+ }
+}
+```
+
+```bash
+# Change relay state for a specific duration (duration is optional)
+curl -X POST \
+ -F "state=on" \
+ -F "duration=20000" \
+ http:///api/router/relay1
+```
+
+```bash
+# Set the duty of the dimmer
+curl -X POST \
+ -F "duty=4095" \
+ http:///api/router/output1/dimmer
+```
+
+```bash
+# Set the duty cycle of the dimmer [0.0, 100.0]
+curl -X POST \
+ -F "duty_cycle=50.55" \
+ http:///api/router/output1/dimmer
+```
+
+```bash
+# Change bypass relay state
+curl -X POST \
+ -F "state=on" \
+ http:///api/router/output1/bypass
+```
+
+## `/api/system`
+
+System information: device, memory usage, network, application, router temperature, etc
+
+```bash
+curl -X GET http:///api/system
+```
+
+```json
+{
+ "app": {
+ "manufacturer": "Mathieu Carbou",
+ "model": "Pro",
+ "name": "YaSolR",
+ "version": "main_7292f69_modified"
+ },
+ "device": {
+ "boots": 540,
+ "cores": 2,
+ "cpu_freq": 240,
+ "heap": {
+ "total": 262380,
+ "usage": 53.27999878,
+ "used": 139788
+ },
+ "id": "A1C48",
+ "model": "ESP32-D0WD",
+ "revision": 301,
+ "uptime": 42
+ },
+ "firmware": {
+ "build": {
+ "branch": "main",
+ "hash": "7292f69",
+ "timestamp": "2024-07-14T15:03:30.849885+00:00"
+ },
+ "debug": true,
+ "filename": "YaSolR-main-pro-esp32-debug.bin"
+ },
+ "network": {
+ "eth": {
+ "ip_address": "0.0.0.0",
+ "mac_address": ""
+ },
+ "hostname": "yasolr-a1c48",
+ "ip_address": "192.168.125.123",
+ "mac_address": "B0:B2:1C:0A:1C:48",
+ "mode": "wifi",
+ "ntp": "on",
+ "wifi": {
+ "bssid": "00:17:13:37:28:C0",
+ "ip_address": "192.168.125.123",
+ "mac_address": "B0:B2:1C:0A:1C:48",
+ "quality": 100,
+ "rssi": -30,
+ "ssid": "IoT"
+ }
+ }
+}
+```
+
+## `/api/system/reset`
+
+```bash
+# System Restart
+curl -X POST http:///api/system/restart
+```
+
+## `/api/restart`
+
+```bash
+# System Factory Reset
+curl -X POST http:///api/system/reset
+```
diff --git "a/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf" "b/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf"
new file mode 100644
index 0000000..1b30c5f
Binary files /dev/null and "b/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf" differ
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp
new file mode 100644
index 0000000..5a254d1
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp
@@ -0,0 +1,238 @@
+// ********************
+// Gestion des Actions
+// ********************
+#include
+#include "Actions.h"
+#include "EEPROM.h"
+#include
+
+
+
+//Class Action
+Action::Action() {
+ Gpio = -1;
+}
+Action::Action(int aIdx) {
+ Gpio = -1; // si le n° de pin n'est pas valid, on ne fait rien
+ Idx = aIdx;
+ T_LastAction = int(millis() / 1000);
+ On = false;
+ Actif = 0;
+ Reactivite = 50;
+ OutOn = 1;
+ OutOff = 0;
+ Tempo = 0;
+ Repet = 0;
+ tOnOff = 0;
+}
+
+
+
+void Action::Arreter() {
+ int Tseconde = int(millis() / 1000);
+ if ((Tseconde - T_LastAction) >= Tempo || Idx == 0 || Actif != 1) {
+ if (Gpio > 0 || Idx == 0) {
+ digitalWrite(Gpio, OutOff);
+ T_LastAction = Tseconde;
+ } else {
+ if (On || ((Tseconde - T_LastAction) > Repet && Repet != 0)) {
+ CallExterne(Host, OrdreOff, Port);
+ T_LastAction = Tseconde;
+ }
+ }
+ On = false;
+ }
+}
+void Action::RelaisOn() {
+ int Tseconde = int(millis() / 1000);
+ if ((Tseconde - T_LastAction) >= Tempo) {
+ if (Gpio > 0) {
+ digitalWrite(Gpio, OutOn);
+ T_LastAction = Tseconde;
+ On = true;
+ } else {
+ if (Actif == 1) {
+ if (!On || ((Tseconde - T_LastAction) > Repet && Repet != 0)) {
+ CallExterne(Host, OrdreOn, Port);
+ T_LastAction = Tseconde;
+ }
+ On = true;
+ }
+ }
+ }
+}
+void Action::Prioritaire() {
+ int tempo_ = Tempo;
+ if (tOnOff != 0) {
+ Tempo = 0;
+ if (tOnOff > 0) {
+ RelaisOn();
+ } else {
+ Arreter();
+ }
+ Tempo = tempo_;
+ }
+}
+
+void Action::Definir(String ligne) {
+ String RS = String((char)30); //Record Separator
+ Actif = byte(ligne.substring(0, ligne.indexOf(RS)).toInt());
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Titre = ligne.substring(0, ligne.indexOf(RS));
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Host = ligne.substring(0, ligne.indexOf(RS));
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Port = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ OrdreOn = ligne.substring(0, ligne.indexOf(RS));
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ OrdreOff = ligne.substring(0, ligne.indexOf(RS));
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Repet = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ Repet = min(Repet, 32000);
+ Repet = max(0, Repet);
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Tempo = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ Tempo = min(Tempo, 32000);
+ Tempo = max(0, Tempo);
+ if (Repet > 0) {
+ Repet = max(Tempo + 4, Repet); //Pour eviter conflit
+ }
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Reactivite = byte(ligne.substring(0, ligne.indexOf(RS)).toInt());
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ NbPeriode = byte(ligne.substring(0, ligne.indexOf(RS)).toInt());
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ int Hdeb_ = 0;
+ for (byte i = 0; i < NbPeriode; i++) {
+ Type[i] = byte(ligne.substring(0, ligne.indexOf(RS)).toInt()); //NO,OFF,ON,PW,Triac
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Hfin[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ Hdeb[i] = Hdeb_;
+ Hdeb_ = Hfin[i];
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Vmin[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Vmax[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Tinf[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Tsup[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ Tarif[i] = ligne.substring(0, ligne.indexOf(RS)).toInt();
+ ligne = ligne.substring(ligne.indexOf(RS) + 1);
+ }
+}
+String Action::Lire() {
+ String GS = String((char)29); //Group Separator
+ String RS = String((char)30); //Record Separator
+ String S;
+ S += String(Actif) + RS;
+ S += Titre + RS;
+ S += Host + RS;
+ S += String(Port) + RS;
+ S += OrdreOn + RS;
+ S += OrdreOff + RS;
+ S += String(Repet) + RS;
+ S += String(Tempo) + RS;
+ S += String(Reactivite) + RS;
+ S += String(NbPeriode) + RS;
+ for (byte i = 0; i < NbPeriode; i++) {
+ S += String(Type[i]) + RS;
+ S += String(Hfin[i]) + RS;
+ S += String(Vmin[i]) + RS;
+ S += String(Vmax[i]) + RS;
+ S += String(Tinf[i]) + RS;
+ S += String(Tsup[i]) + RS;
+ S += String(Tarif[i]) + RS;
+ }
+ return S + GS;
+}
+
+
+
+
+byte Action::TypeEnCours(int Heure, float Temperature, int Ltarfbin) { //Retourne type d'action active Ă cette heure et test temperature OK
+ byte S = 1;
+ bool TemperatureOk;
+ bool TarifOk;
+ for (int i = 0; i < NbPeriode; i++) {
+ TemperatureOk = true;
+ if (Temperature > -100) {
+ if (Tinf[i] <= 1000 && Temperature * 10 > Tinf[i]) { TemperatureOk = false; }
+ if (Tsup[i] <= 1000 && Temperature * 10 < Tsup[i]) { TemperatureOk = false; }
+ }
+ TarifOk = true;
+ if (Ltarfbin > 0 && (Ltarfbin & Tarif[i]) == 0) TarifOk = false;
+ if (Heure >= Hdeb[i] && Heure <= Hfin[i] && TemperatureOk && TarifOk) S = Type[i];
+ }
+ if (tOnOff > 0) S = 2; // Force On
+ if (tOnOff < 0) S = 1; // Force Off
+ return S; //0=NO,1=OFF,2=ON,3=PW,4=Triac
+}
+int Action::Valmin(int Heure) { //Retourne la valeur Vmin (ex seuil Triac) Ă cette heure
+ int S = 0;
+ for (int i = 0; i < NbPeriode; i++) {
+ if (Heure >= Hdeb[i] && Heure <= Hfin[i]) {
+ S = Vmin[i];
+ }
+ }
+ return S;
+}
+int Action::Valmax(int Heure) { //Retourne la valeur Vmax (ex ouverture du Triac) Ă cette heure
+ int S = 0;
+ for (int i = 0; i < NbPeriode; i++) {
+ if (Heure >= Hdeb[i] && Heure <= Hfin[i]) {
+ S = Vmax[i];
+ }
+ }
+ return S;
+}
+
+void Action::InitGpio() { //Initialise les sorties GPIO pour des relais
+ int p;
+ String S;
+ String IS = String((char)31); //Input Separator
+
+ if (Idx > 0) {
+ T_LastAction = 0;
+ Gpio = -1;
+ p = OrdreOn.indexOf(IS);
+ if (p >= 0) {
+ Gpio = OrdreOn.substring(0, p).toInt();
+ OutOn = OrdreOn.substring(p+1).toInt();
+ OutOff=(1+OutOn)%2;
+ if (Gpio > 0) {
+ pinMode(Gpio, OUTPUT);
+ digitalWrite(Gpio, OutOff);
+ }
+ }
+ }
+}
+void Action::CallExterne(String host, String url, int port) {
+ if (url != "") {
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientExt;
+ char hostbuf[host.length() + 1];
+ host.toCharArray(hostbuf, host.length() + 1);
+
+ if (!clientExt.connect(hostbuf, port)) {
+ StockMessage("connection to :" + host + " failed");
+ return;
+ }
+ clientExt.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientExt.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage(">>> clientESP_Ext Timeout ! : " + host);
+ clientExt.stop();
+ return;
+ }
+ }
+
+ // Read all the lines of the reply from server
+ while (clientExt.available()) {
+ String line = clientExt.readStringUntil('\r');
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h
new file mode 100644
index 0000000..26d86f8
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h
@@ -0,0 +1,58 @@
+// ********************
+// Gestion des Actions
+// ********************
+class Action {
+private:
+ int Idx; //Index
+ void CallExterne(String host,String url, int port);
+ int T_LastAction=0;
+ int tempoTimer=0;
+
+
+
+public:
+ Action(); //Constructeur par defaut
+ Action(int aIdx);
+
+ void Definir(String ligne);
+ String Lire();
+ void Activer(float Pw, int Heure, float Temperature, int Ltarfbin);
+ void Arreter();
+ void RelaisOn();
+ void Prioritaire();
+
+
+ byte TypeEnCours(int Heure,float Temperature, int Ltarfbin);
+ int Valmin(int Heure);
+ int Valmax(int Heure);
+ void InitGpio();
+ byte Actif;
+ int Port;
+ int Repet;
+ int Tempo;
+ String Titre;
+ String Host;
+ String OrdreOn;
+ String OrdreOff;
+ int Gpio;
+ int OutOn;
+ int OutOff;
+ int tOnOff;
+ byte Reactivite;
+ byte NbPeriode;
+ bool On;
+ byte Type[8];
+ int Hdeb[8];
+ int Hfin[8];
+ int Vmin[8];
+ int Vmax[8];
+ int Tinf[8];
+ int Tsup[8];
+ byte Tarif[8];
+
+};
+
+
+ extern void StockMessage(String m);
+
+
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino b/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino
new file mode 100644
index 0000000..534e0d4
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino
@@ -0,0 +1,265 @@
+// **********************************************************************************************
+// * MQTT AUTO-DISCOVERY POUR HOME ASSISTANT ou DOMOTICZ *
+// **********************************************************************************************
+bool Discovered = false;
+char DEVICE[300];
+char ESP_ID[15];
+char mdl[30];
+char StateTopic[50];
+
+
+// Types de composants reconnus par HA et obligatoires pour l'Auto-Discovery.
+const char *SSR = "sensor";
+const char *SLCT = "select";
+const char *NB = "number";
+const char *BINS = "binary_sensor";
+const char *SWTC = "switch";
+const char *TXT = "text";
+void GestionMQTT() {
+ if (MQTTRepet > 0 || Source_Temp == "tempMqtt" || Source == "Pmqtt" || subMQTT == 1) {
+ if (testMQTTconnected()) {
+ clientMQTT.loop();
+ envoiVersMQTT();
+ }
+ }
+}
+
+bool testMQTTconnected() {
+ bool connecte = true;
+ if (!clientMQTT.connected()) { // si le mqtt n'est pas connecté (utile aussi lors de la 1ere connexion)
+ Serial.println("Connection au serveur MQTT ...");
+ byte arr[4];
+ arr[0] = MQTTIP & 0xFF; // 0x78
+ arr[1] = (MQTTIP >> 8) & 0xFF; // 0x56
+ arr[2] = (MQTTIP >> 16) & 0xFF; // 0x34
+ arr[3] = (MQTTIP >> 24) & 0xFF; // 0x12
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ clientMQTT.setServer(host.c_str(), MQTTPort);
+ clientMQTT.setCallback(callback); //DĂ©claration de la fonction de souscription
+ if (clientMQTT.connect(MQTTdeviceName.c_str(), MQTTUser.c_str(), MQTTPwd.c_str())) { // si l'utilisateur est connecté au mqtt
+ StockMessage(MQTTdeviceName + " connecté au broker MQTT");
+ if (Source_Temp == "tempMqtt") {
+ char TopicV[50];
+ sprintf(TopicV, "%s", TopicT.c_str());
+ clientMQTT.subscribe(TopicV);
+ }
+ if (Source == "Pmqtt") {
+ char Topicp[50];
+ sprintf(Topicp, "%s", TopicP.c_str());
+ clientMQTT.subscribe(Topicp);
+ }
+ if (subMQTT == 1) {
+ for (int i = 0; i < NbActions; i++) {
+ if (LesActions[i].Actif > 0) {
+ char TopicAct[50];
+ sprintf(TopicAct, "%s", LesActions[i].Titre.c_str());
+ clientMQTT.subscribe(TopicAct);
+ }
+ }
+ }
+ sprintf(StateTopic, "%s/%s_state", MQTTPrefix.c_str(), MQTTdeviceName.c_str());
+ byte mac[6]; // the MAC address of your Wifi shield
+ WiFi.macAddress(mac);
+ sprintf(ESP_ID, "%02x%02x%02x", mac[2], mac[1], mac[0]); // ID de l'entité pour HA
+ sprintf(mdl, "%s%s", "ESP32 - ", ESP_ID); // ID de l'entité pour HA
+ String mf = "F1ATB - https://f1atb.fr";
+ String cu = "http://" + WiFi.localIP().toString();
+ String hw = String(ESP.getChipModel()) + " rev." + String(ESP.getChipRevision());
+ String sw = Version;
+ sprintf(DEVICE, "{\"ids\":\"%s\",\"name\":\"%s\",\"mdl\":\"%s\",\"mf\":\"%s\",\"hw\":\"%s\",\"sw\":\"%s\",\"cu\":\"%s\"}", ESP_ID, nomRouteur.c_str(), mdl, mf.c_str(), hw.c_str(), sw.c_str(), cu.c_str());
+
+ } else { // si utilisateur pas connecté au mqtt
+ StockMessage("Echec connexion MQTT : " + host);
+ connecte = false;
+ delay(100);
+ previousMQTTMillis=millis();
+ }
+ }
+ return connecte;
+}
+void envoiVersMQTT() {
+ unsigned long tps = millis();
+ int etat = 0; // utilisé pour l'envoie de l'état On/Off des actions.
+ if (int((tps - previousMQTTenvoiMillis) / 1000) > MQTTRepet && MQTTRepet != 0) { // Si Service MQTT activé avec période sup à 0
+ previousMQTTenvoiMillis = tps;
+ if (!Discovered) { //(uniquement au démarrage discovery = 0)
+ sendMQTTDiscoveryMsg_global();
+ }
+ SendDataToHomeAssistant(); // envoie du Payload au State topic
+ clientMQTT.loop();
+ }
+}
+//Callback après souscription à un topic et réaliser une action
+void callback(char *topic, byte *payload, unsigned int length) {
+ char Message[length + 1];
+ for (int i = 0; i < length; i++) {
+ Message[i] = payload[i];
+ }
+ Message[length] = '\0';
+ String message = String(Message) + ",";
+ if (String(topic) == TopicT && Source_Temp == "tempMqtt") { //Temperature attendue
+ temperature = ValJson("temperature", message);
+ TemperatureValide = 5;
+ }
+ if (String(topic) == TopicP && Source == "Pmqtt") { //Mesure de puissance
+ PwMQTT = ValJson("Pw", message);
+ PvaMQTT = ValJson("Pva", message);
+ PfMQTT = ValJson("Pf", message);
+ P_MQTT_Brute = String(Message);
+ if (message.indexOf("Pw") > 0) LastPwMQTTMillis = millis();
+ }
+ if (subMQTT == 1) {
+ for (int i = 0; i < NbActions; i++) {
+ if (LesActions[i].Actif > 0 && LesActions[i].Titre == String(topic)) {
+ LesActions[i].tOnOff=ValJson("tOnOff", message);
+ LesActions[i].Prioritaire();
+ }
+ }
+ }
+ Serial.print(topic);
+ Serial.println(Message);
+}
+//*************************************************************************
+//* CONFIG OF DISCOVERY MESSAGE FOR HOME ASSISTANT / DOMOTICZ *
+//*************************************************************************
+
+
+void sendMQTTDiscoveryMsg_global() {
+ String ActType;
+ // augmente la taille du buffer wifi Mqtt (voir PubSubClient.h)
+ clientMQTT.setBufferSize(700); // voir -->#define MQTT_MAX_PACKET_SIZE 256 is the default value in PubSubClient.h
+ if (Source == "UxIx2" || Source == "ShellyEm") {
+ DeviceToDiscover("PuissanceS_T", "W", "power", "0");
+ DeviceToDiscover("PuissanceI_T", "W", "power", "0");
+ DeviceToDiscover("Tension_T", "V", "voltage", "2");
+ DeviceToDiscover("Intensite_T", "A", "current", "2");
+ DeviceToDiscover("PowerFactor_T", "", "power_factor", "2");
+ DeviceToDiscover("Energie_T_Soutiree", "Wh", "energy", "0");
+ DeviceToDiscover("Energie_T_Injectee", "Wh", "energy", "0");
+ DeviceToDiscover("EnergieJour_T_Soutiree", "Wh", "energy", "0");
+ DeviceToDiscover("EnergieJour_T_Injectee", "Wh", "energy", "0");
+ DeviceToDiscover("Frequence", "Hz", "frequency", "2");
+ }
+ if (Source_Temp != "tempNo") DeviceToDiscover("Temperature", "°C", "temperature", "1");
+
+
+ if (Source == "Linky") {
+ DeviceTextToDiscover("LTARF", "Option Tarifaire");
+ DeviceToDiscover("Code_Tarifaire", "", "", "0");
+ }
+ if (Source == "Enphase") {
+ DeviceToDiscover("PactProd", "W", "power", "0");
+ DeviceToDiscover("PactConso_M", "W", "power", "0");
+ }
+
+ DeviceToDiscover("PuissanceS_M", "W", "power", "0");
+ DeviceToDiscover("PuissanceI_M", "W", "power", "0");
+ DeviceToDiscover("Tension_M", "V", "voltage", "2");
+ DeviceToDiscover("Intensite_M", "A", "current", "2");
+ DeviceToDiscover("PowerFactor_M", "", "power_factor", "2");
+ DeviceToDiscover("Energie_M_Soutiree", "Wh", "energy", "0");
+ DeviceToDiscover("Energie_M_Injectee", "Wh", "energy", "0");
+ DeviceToDiscover("EnergieJour_M_Soutiree", "Wh", "energy", "0");
+ DeviceToDiscover("EnergieJour_M_Injectee", "Wh", "energy", "0");
+
+ for (int i = 0; i < NbActions; i++) {
+ ActType = "Ouverture_Relais_" + String(i);
+ if (i == 0) ActType = "OuvertureTriac";
+ DeviceToDiscover(ActType, "%", "power_factor", "0"); //Type power factor pour etre accepté par HA
+ }
+
+
+ Serial.println("Paramètres Auto-Discovery publiés !");
+
+ //clientMQTT.setBufferSize(512); // go to initial value wifi/mqtt buffer
+ Discovered = true;
+
+
+} // END OF sendMQTTDiscoveryMsg_global
+
+void DeviceToDiscover(String VarName, String Unit, String Class, String Round) {
+ char value[700];
+ char DiscoveryTopic[120];
+ char UniqueID[50];
+ char ValTpl[60];
+ char state_class[60];
+ String TitleName = String(MQTTdeviceName) + " " + String(VarName);
+ sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), SSR, MQTTdeviceName.c_str(), VarName.c_str(), "config");
+ sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str());
+ sprintf(ValTpl, "{{ value_json.%s|default(0)|round(%s)}}", VarName.c_str(), Round.c_str());
+ sprintf(state_class, "%s", "");
+ if (Unit == "Wh" || Unit == "kWh") {
+ sprintf(state_class, "\"state_class\":\"total_increasing\"%s,", state_class);
+ }
+ sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"device_class\": \"%s\",\"unit_of_meas\": \"%s\",%s\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, Class.c_str(), Unit.c_str(), state_class, ValTpl, DEVICE);
+ clientMQTT.publish(DiscoveryTopic, value);
+}
+void DeviceBinToDiscover(String VarName, String TitleName) {
+ char value[700];
+ char DiscoveryTopic[120];
+ char UniqueID[50];
+ char ValTpl[60];
+ String init = "OFF"; // default value
+ String ic = "mdi:electric-switch";
+ sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), BINS, MQTTdeviceName.c_str(), VarName.c_str(), "config");
+ sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str());
+ sprintf(ValTpl, "{{ value_json.%s}}", VarName.c_str());
+ sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"init\": \"%s\",\"ic\": \"%s\",\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, init.c_str(), ic.c_str(), ValTpl, DEVICE);
+ clientMQTT.publish(DiscoveryTopic, value);
+}
+
+void DeviceTextToDiscover(String VarName, String TitleName) {
+ char value[600];
+ char DiscoveryTopic[120];
+ char UniqueID[50];
+ char ValTpl[50];
+ sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), SSR, MQTTdeviceName.c_str(), VarName.c_str(), "config");
+ sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str());
+ sprintf(ValTpl, "{{ value_json.%s }}", VarName.c_str());
+ sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"device_class\": \"%s\",\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, "enum", ValTpl, DEVICE);
+ clientMQTT.publish(DiscoveryTopic, value);
+}
+//****************************************
+//* ENVOIE DES DATAS VERS HOME ASSISTANT *
+//****************************************
+
+void SendDataToHomeAssistant() {
+ String ActType;
+ char value[1000];
+ sprintf(value, "{\"PuissanceS_M\": %d, \"PuissanceI_M\": %d, \"Tension_M\": %.1f, \"Intensite_M\": %.1f, \"PowerFactor_M\": %.2f, \"Energie_M_Soutiree\":%d,\"Energie_M_Injectee\":%d, \"EnergieJour_M_Soutiree\":%d, \"EnergieJour_M_Injectee\":%d", PuissanceS_M, PuissanceI_M, Tension_M, Intensite_M, PowerFactor_M, Energie_M_Soutiree, Energie_M_Injectee, EnergieJour_M_Soutiree, EnergieJour_M_Injectee);
+
+ if (Source == "UxIx2" || Source == "ShellyEm") {
+ sprintf(value, "%s,\"PuissanceS_T\": %d, \"PuissanceI_T\": %d, \"Tension_T\": %.1f, \"Intensite_T\": %.1f, \"PowerFactor_T\": %.2f, \"Energie_T_Soutiree\":%d,\"Energie_T_Injectee\":%d, \"EnergieJour_T_Soutiree\":%d, \"EnergieJour_T_Injectee\":%d, \"Frequence\":%.2f", value, PuissanceS_T, PuissanceI_T, Tension_T, Intensite_T, PowerFactor_T, Energie_T_Soutiree, Energie_T_Injectee, EnergieJour_T_Soutiree, EnergieJour_T_Injectee, Frequence);
+ }
+ if (temperature > -100 && Source_Temp != "tempNo") {
+ sprintf(value, "%s,\"Temperature\": %.1f", value, temperature);
+ }
+ if (Source == "Linky") {
+ int code = 0;
+ if (LTARF.indexOf("HEURE CREUSE") >= 0) code = 1; //Code Linky
+ if (LTARF.indexOf("HEURE PLEINE") >= 0) code = 2;
+ if (LTARF.indexOf("HC BLEU") >= 0) code = 11;
+ if (LTARF.indexOf("HP BLEU") >= 0) code = 12;
+ if (LTARF.indexOf("HC BLANC") >= 0) code = 13;
+ if (LTARF.indexOf("HP BLANC") >= 0) code = 14;
+ if (LTARF.indexOf("HC ROUGE") >= 0) code = 15;
+ if (LTARF.indexOf("HP ROUGE") >= 0) code = 16;
+ if (LTARF.indexOf("TEMPO_BLEU") >= 0) code = 17; // Code EDF
+ if (LTARF.indexOf("TEMPO_BLANC") >= 0) code = 18;
+ if (LTARF.indexOf("TEMPO_ROUGE") >= 0) code = 19;
+ sprintf(value, "%s,\"LTARF\":\"%s\", \"Code_Tarifaire\":%d", value, LTARF, code);
+ }
+
+ if (Source == "Enphase") {
+ sprintf(value, "%s,\"PactProd\":%d, \"PactConso_M\":%d", value, PactProd, PactConso_M);
+ }
+
+ for (int i = 0; i < NbActions; i++) {
+ ActType = "Ouverture_Relais_" + String(i);
+ if (i == 0) ActType = "OuvertureTriac";
+ int Ouv = 100 - Retard[i];
+ sprintf(value, "%s,\"%s\":%d", value, ActType.c_str(), Ouv);
+ }
+ sprintf(value, "%s}", value);
+ bool published = clientMQTT.publish(StateTopic, value);
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino
new file mode 100644
index 0000000..a57322c
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino
@@ -0,0 +1,540 @@
+// ***************
+// * WEB SERVER *
+// ***************
+void Init_Server() {
+ //Init Web Server on port 80
+ server.on("/", handleRoot);
+ server.on("/MainJS", handleMainJS);
+ server.on("/Para", handlePara);
+ server.on("/ParaJS", handleParaJS);
+ server.on("/ParaRouteurJS", handleParaRouteurJS);
+ server.on("/ParaAjax", handleParaAjax);
+ server.on("/ParaRouteurAjax", handleParaRouteurAjax);
+ server.on("/ParaUpdate", handleParaUpdate);
+ server.on("/Actions", handleActions);
+ server.on("/ActionsJS", handleActionsJS);
+ server.on("/ActionsJS2", handleActionsJS2);
+ server.on("/ActionsUpdate", handleActionsUpdate);
+ server.on("/ActionsAjax", handleActionsAjax);
+ server.on("/Brute", handleBrute);
+ server.on("/BruteJS", handleBruteJS);
+ server.on("/ajax_histo48h", handleAjaxHisto48h);
+ server.on("/ajax_histo1an", handleAjaxHisto1an);
+ server.on("/ajax_dataRMS", handleAjaxRMS);
+ server.on("/ajax_dataESP32", handleAjaxESP32);
+ server.on("/ajax_data", handleAjaxData);
+ server.on("/ajax_data10mn", handleAjaxData10mn);
+ server.on("/ajax_etatActions", handleAjax_etatActions);
+ server.on("/ajax_Temperature", handleAjaxTemperature);
+ server.on("/SetGPIO", handleSetGpio);
+ server.on("/restart", handleRestart);
+ server.on("/Change_Wifi", handleChange_Wifi);
+ server.on("/AP_ScanWifi", handleAP_ScanWifi);
+ server.on("/AP_SetWifi", handleAP_SetWifi);
+ server.onNotFound(handleNotFound);
+
+ //SERVER OTA
+ server.on("/OTA", HTTP_GET, []() {
+ server.sendHeader("Connection", "close");
+ server.send(200, "text/html", OtaHtml);
+ });
+ /*handling uploading firmware file */
+ server.on(
+ "/update", HTTP_POST, []() {
+ server.sendHeader("Connection", "close");
+ server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
+ ESP.restart();
+ },
+ []() {
+ HTTPUpload& upload = server.upload();
+ if (upload.status == UPLOAD_FILE_START) {
+ Serial.printf("Update: %s\n", upload.filename.c_str());
+ if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
+ Update.printError(Serial);
+ }
+ } else if (upload.status == UPLOAD_FILE_WRITE) {
+ /* flashing firmware to ESP*/
+ if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
+ Update.printError(Serial);
+ }
+ } else if (upload.status == UPLOAD_FILE_END) {
+ if (Update.end(true)) { //true to set the size to the current progress
+ Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
+ } else {
+ Update.printError(Serial);
+ }
+ }
+ });
+
+
+ server.begin();
+}
+
+
+void handleRoot() { //Pages principales
+ if (WiFi.getMode() != WIFI_STA) { // en AP et STA mode
+ server.send(200, "text/html", String(ConnectAP_Html));
+ } else { //Station Mode seul
+ server.send(200, "text/html", String(MainHtml));
+ }
+}
+void handleChange_Wifi(){
+ server.send(200, "text/html", String(ConnectAP_Html));
+}
+void handleMainJS() { //Code Javascript
+ server.send(200, "text/html", String(MainJS)); // Javascript code
+}
+void handleBrute() { //Page données brutes
+ server.send(200, "text/html", String(PageBrute));
+}
+void handleBruteJS() { //Code Javascript
+ server.send(200, "text/html", String(PageBruteJS)); // Javascript code
+}
+
+void handleAjaxRMS() { // Envoi des dernières données brutes reçues du RMS
+ String S = "";
+ String RMSExtDataB = "";
+ int LastIdx = server.arg(0).toInt();
+ if (Source == "Ext") {
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientESP_RMS;
+ byte arr[4];
+ arr[0] = RMSextIP & 0xFF; // 0x78
+ arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56
+ arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34
+ arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12
+
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ if (!clientESP_RMS.connect(host.c_str(), 80)) {
+ StockMessage("connection to ESP_RMS external failed (call from handleAjaxRMS)");
+ return;
+ }
+ String url = "/ajax_dataRMS?idx=" + String(LastIdx);
+ clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientESP_RMS.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage(">>> clientESP_RMS Timeout !");
+ clientESP_RMS.stop();
+ return;
+ }
+ }
+ // Lecture des données brutes distantes
+ while (clientESP_RMS.available()) {
+ RMSExtDataB += clientESP_RMS.readStringUntil('\r');
+ }
+ S = RMSExtDataB.substring(RMSExtDataB.indexOf("\n\n") + 2);
+ } else {
+ S = DATE + RS + Source_data;
+ if (Source_data == "UxI") {
+ S += RS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PowerFactor_M) + GS;
+ int i0 = 0;
+ int i1 = 0;
+ for (int i = 0; i < 100; i++) {
+ i1 = (i + 1) % 100;
+ if (voltM[i] <= 0 && voltM[i1] > 0) {
+ i0 = i1; //Point de départ tableau . Phase positive
+ i = 100;
+ }
+ }
+ for (int i = 0; i < 100; i++) {
+ i1 = (i + i0) % 100;
+ S += String(int(10 * voltM[i1])) + RS; //Voltages*10. Increase dynamic
+ }
+ S += "0" + GS;
+ for (int i = 0; i < 100; i++) {
+ i1 = (i + i0) % 100;
+ S += String(int(10 * ampM[i1])) + RS; //Currents*10
+ }
+ S += "0";
+ }
+ if (Source_data == "UxIx2") {
+
+ S += GS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PuissanceS_M - PuissanceI_M) + RS + String(PowerFactor_M) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee);
+ S += RS + String(Tension_T) + RS + String(Intensite_T) + RS + String(PuissanceS_T - PuissanceI_T) + RS + String(PowerFactor_T) + RS + String(Energie_T_Soutiree) + RS + String(Energie_T_Injectee);
+ S += RS + String(Frequence);
+ }
+ if (Source_data == "Linky") {
+ S += GS;
+ while (LastIdx != IdxDataRawLinky) {
+ S += String(DataRawLinky[LastIdx]);
+ LastIdx = (1 + LastIdx) % 10000;
+ }
+ S += GS + String(IdxDataRawLinky);
+ }
+ if (Source_data == "Enphase") {
+ S += GS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PuissanceS_M - PuissanceI_M) + RS + String(PowerFactor_M) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee);
+ S += RS + String(PactProd) + RS + String(PactConso_M);
+ String SessionId = "Not Received from Enphase";
+ if (Session_id != "") {
+ SessionId = "Ok Received from Enphase";
+ }
+ String Token_Enphase = "Not Received from Enphase";
+ if (TokenEnphase.length() > 50) {
+ Token_Enphase = "Ok Received from Enphase";
+ }
+ if (EnphaseUser == "") {
+ SessionId = "Not Requested";
+ Token_Enphase = "Not Requested";
+ }
+ S += RS + SessionId;
+
+ S += RS + Token_Enphase;
+ }
+ if (Source_data == "SmartG") {
+ S += GS + SG_dataBrute;
+ }
+ if (Source_data == "ShellyEm") {
+ S += GS + ShEm_dataBrute;
+ }
+ if (Source_data == "UxIx3") {
+ S += GS + MK333_dataBrute;
+ }
+ if (Source_data == "Pmqtt") {
+ S += GS + P_MQTT_Brute;
+ }
+ }
+
+ server.send(200, "text/html", S);
+}
+void handleAjaxHisto48h() { // Envoi Historique de 50h (600points) toutes les 5mn
+ String S = "";
+ String T = "";
+ String U = "";
+ String Ouverture = "";
+ int iS = IdxStockPW;
+ for (int i = 0; i < 600; i++) {
+ S += String(tabPw_Maison_5mn[iS]) + ",";
+ T += String(tabPw_Triac_5mn[iS]) + ",";
+ U += String(float(tabTemperature_5mn[iS]) * 0.1) + ",";
+ iS = (1 + iS) % 600;
+ }
+ for (int i = 0; i < NbActions; i++) {
+ if ((LesActions[i].Actif > 0) && (ITmode > 0 || i > 0)) {
+ iS = IdxStockPW;
+ if (LesActions[i].Actif > 0) {
+ Ouverture += GS;
+ for (int j = 0; j < 600; j++) {
+ Ouverture += String(tab_histo_ouverture[i][iS]) + RS;
+ iS = (1 + iS) % 600;
+ }
+ Ouverture += LesActions[i].Titre;
+ }
+ }
+ }
+ server.send(200, "text/html", Source_data + GS + S + GS + T + GS + String(temperature) + GS + U + Ouverture);
+}
+void handleAjaxESP32() { // Envoi des dernières infos sur l'ESP32
+ IT10ms = 0;
+ IT10ms_in = 0;
+ String S = "";
+ float H = float(T_On_seconde) / 3600;
+ String coeur0 = String(int(previousTimeRMSMin)) + ", " + String(int(previousTimeRMSMoy)) + ", " + String(int(previousTimeRMSMax));
+ String coeur1 = String(int(previousLoopMin)) + ", " + String(int(previousLoopMoy)) + ", " + String(int(previousLoopMax));
+ S += String(H) + RS + WiFi.RSSI() + RS + WiFi.BSSIDstr() + RS + WiFi.macAddress() + RS + ssid + RS + WiFi.localIP().toString() + RS + WiFi.gatewayIP().toString() + RS + WiFi.subnetMask().toString();
+ S += RS + coeur0 + RS + coeur1 + RS + String(P_cent_EEPROM) + RS;
+ delay(15); //Comptage interruptions
+ if (IT10ms_in > 0) {
+ S += String(IT10ms_in) + "/" + String(IT10ms);
+ } else {
+ S += "Pas de Triac";
+ }
+ if (ITmode > 0) {
+ S += RS + "Secteur";
+ } else {
+ S += RS + "Horloge ESP";
+ }
+ int j = idxMessage;
+ for (int i = 0; i < 10; i++) {
+ S += RS + MessageH[j];
+ j = (j + 1) % 10;
+ }
+ server.send(200, "text/html", S);
+}
+void handleAjaxHisto1an() { // Envoi Historique Energie quotiiienne sur 1 an 370 points
+ server.send(200, "text/html", HistoriqueEnergie1An());
+}
+void handleAjaxData() { //Données page d'accueil
+ String DateLast = "Attente de l'heure par Internet";
+ if (DATEvalid) {
+ DateLast = DATE;
+ }
+ String S = "Deb" + RS + DateLast + RS + Source_data + RS + LTARF + RS + STGE + RS + String(temperature) + RS + String(Pva_valide);
+ S += GS + String(PuissanceS_M) + RS + String(PuissanceI_M) + RS + String(PVAS_M) + RS + String(PVAI_M);
+ S += RS + String(EnergieJour_M_Soutiree) + RS + String(EnergieJour_M_Injectee) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee);
+ if (Source_data == "UxIx2" || (Source_data == "ShellyEm" && EnphaseSerial.toInt() < 3)) { //UxIx2 ou Shelly monophasé avec 2 sondes
+ S += GS + String(PuissanceS_T) + RS + String(PuissanceI_T) + RS + String(PVAS_T) + RS + String(PVAI_T);
+ S += RS + String(EnergieJour_T_Soutiree) + RS + String(EnergieJour_T_Injectee) + RS + String(Energie_T_Soutiree) + RS + String(Energie_T_Injectee);
+ }
+ S += GS + "Fin";
+ server.send(200, "text/html", S);
+}
+void handleAjax_etatActions() {
+ int Force = server.arg("Force").toInt();
+ int NumAction = server.arg("NumAction").toInt();
+ if (Force != 0) {
+ if (Force > 0) {
+ if (LesActions[NumAction].tOnOff < 0) {
+ LesActions[NumAction].tOnOff = 0;
+ } else {
+ LesActions[NumAction].tOnOff += 30;
+ }
+ } else {
+ if (LesActions[NumAction].tOnOff > 0) {
+ LesActions[NumAction].tOnOff = 0;
+ } else {
+ LesActions[NumAction].tOnOff -= 30;
+ }
+ }
+ LesActions[NumAction].Prioritaire();
+ }
+ int NbActifs = 0;
+ String S = "";
+ String On_;
+ for (int i = 0; i < NbActions; i++) {
+ if ((LesActions[i].Actif > 0) && (ITmode > 0 || i > 0)) { //Pas de Triac en synchro horloge interne
+ S += String(i) + RS + LesActions[i].Titre + RS;
+ if (LesActions[i].Actif == 1 && i > 0) {
+ if (LesActions[i].On) {
+ S += "On" + RS;
+ } else {
+ S += "Off" + RS;
+ }
+ } else {
+ S += String(100 - Retard[i]) + RS;
+ }
+ S += String(LesActions[i].tOnOff) + RS;
+ S += GS;
+ NbActifs++;
+ }
+ }
+ S = String(temperature) + GS + String(Source_data) + GS + String(RMSextIP) + GS + NbActifs + GS + S;
+ server.send(200, "text/html", S);
+}
+void handleAjaxTemperature() {
+ server.send(200, "text/html", GS + String(temperature) + RS);
+}
+void handleRestart() { // Eventuellement Reseter l'ESP32 Ă distance
+ server.send(200, "text/plain", "OK Reset. Attendez.");
+ delay(1000);
+ ESP.restart();
+}
+void handleAjaxData10mn() { // Envoi Historique de 10mn (300points)Energie Active Soutiré - Injecté
+ String S = "";
+ String T = "";
+ int iS = IdxStock2s;
+ for (int i = 0; i < 300; i++) {
+ S += String(tabPw_Maison_2s[iS]) + ",";
+ S += String(tabPva_Maison_2s[iS]) + ",";
+ T += String(tabPw_Triac_2s[iS]) + ",";
+ T += String(tabPva_Triac_2s[iS]) + ",";
+ iS = (1 + iS) % 300;
+ }
+ server.send(200, "text/html", Source_data + GS + S + GS + T);
+}
+void handleActions() {
+ server.send(200, "text/html", String(ActionsHtml));
+}
+void handleActionsJS() {
+ server.send(200, "text/html", String(ActionsJS));
+}
+void handleActionsJS2() {
+ server.send(200, "text/html", String(ActionsJS2));
+}
+void handleActionsUpdate() {
+ int adresse_max = 0;
+ String s = server.arg("actions");
+ String ligne = "";
+ InitGpioActions(); //RAZ anciennes actions
+ NbActions = 0;
+ while (s.indexOf(GS) > 3 && NbActions < LesActionsLength) {
+ ligne = s.substring(0, s.indexOf(GS));
+ s = s.substring(s.indexOf(GS) + 1);
+ LesActions[NbActions].Definir(ligne);
+ NbActions = NbActions + 1;
+ }
+ adresse_max = EcritureEnROM();
+ server.send(200, "text/plain", "OK" + String(adresse_max));
+ InitGpioActions();
+}
+void handleActionsAjax() {
+ String S = String(temperature) + RS + String(LTARFbin) + RS + String(pTriac) + GS;
+ for (int i = 0; i < NbActions; i++) {
+ S += LesActions[i].Lire();
+ }
+ server.send(200, "text/html", S);
+}
+
+void handlePara() {
+ server.send(200, "text/html", String(ParaHtml));
+}
+void handleParaUpdate() {
+ String Vp[32];
+ String lesparas = server.arg("lesparas") + RS;
+ int idx = 0;
+ while (lesparas.length() > 0) {
+ Vp[idx] = lesparas.substring(0, lesparas.indexOf(RS));
+ lesparas = lesparas.substring(lesparas.indexOf(RS) + 1);
+ idx++;
+ Vp[idx].trim();
+ }
+ dhcpOn = byte(Vp[0].toInt());
+ IP_Fixe = strtoul(Vp[1].c_str(), NULL, 10);
+ Gateway = strtoul(Vp[2].c_str(), NULL, 10);
+ masque = strtoul(Vp[3].c_str(), NULL, 10);
+ dns = strtoul(Vp[4].c_str(), NULL, 10);
+ Source = Vp[5];
+ RMSextIP = strtoul(Vp[6].c_str(), NULL, 10);
+ EnphaseUser = Vp[7];
+ EnphasePwd = Vp[8];
+ EnphaseSerial = Vp[9];
+ TopicP = Vp[10];
+ MQTTRepet = Vp[11].toInt();
+ MQTTIP = strtoul(Vp[12].c_str(), NULL, 10);
+ MQTTPort = Vp[13].toInt(); //2 bytes
+ MQTTUser = Vp[14];
+ MQTTPwd = Vp[15];
+ MQTTPrefix = Vp[16];
+ MQTTdeviceName = Vp[17];
+ subMQTT = byte(Vp[18].toInt());
+ nomRouteur = Vp[19];
+ nomSondeFixe = Vp[20];
+ nomSondeMobile = Vp[21];
+ nomTemperature = Vp[22];
+ Source_Temp = Vp[23];
+ TopicT = Vp[24];
+ IPtemp = strtoul(Vp[25].c_str(), NULL, 10);
+ CalibU = Vp[26].toInt(); //2 bytes
+ CalibI = Vp[27].toInt(); //2 bytes
+ TempoEDFon = byte(Vp[28].toInt());
+ WifiSleep = byte(Vp[29].toInt());
+ pSerial = byte(Vp[30].toInt());
+ pTriac = byte(Vp[31].toInt());
+ int adresse_max = EcritureEnROM();
+ if (Source != "Ext") {
+ Source_data = Source;
+ }
+ server.send(200, "text/plain", "OK" + String(adresse_max));
+ LastHeureEDF = -1;
+}
+void handleParaJS() {
+ server.send(200, "text/html", String(ParaJS));
+}
+void handleParaRouteurJS() {
+ server.send(200, "text/html", String(ParaRouteurJS));
+}
+void handleParaAjax() {
+ String S = String(dhcpOn) + RS + String(IP_Fixe) + RS + String(Gateway) + RS + String(masque) + RS + String(dns) + RS + Source + RS + String(RMSextIP) + RS;
+ S += EnphaseUser + RS + EnphasePwd + RS + EnphaseSerial + RS + TopicP;
+ S += RS + String(MQTTRepet) + RS + String(MQTTIP) + RS + String(MQTTPort) + RS + MQTTUser + RS + MQTTPwd;
+ S += RS + MQTTPrefix + RS + MQTTdeviceName + RS + String(subMQTT) + RS + nomRouteur + RS + nomSondeFixe + RS + nomSondeMobile;
+ S += RS + String(temperature) + RS + nomTemperature + RS + Source_Temp + RS + TopicT + RS + String(IPtemp);
+ S += RS + String(CalibU) + RS + String(CalibI);
+ S += RS + String(TempoEDFon) + RS + String(WifiSleep) + RS + String(pSerial) + RS + String(pTriac);
+ server.send(200, "text/html", S);
+}
+void handleParaRouteurAjax() {
+ String S = Source + RS + Source_data + RS + nomRouteur + RS + Version + RS + nomSondeFixe + RS + nomSondeMobile + RS + String(RMSextIP);
+ S += RS + nomTemperature;
+ server.send(200, "text/html", S);
+}
+void handleSetGpio() {
+ int gpio = server.arg("gpio").toInt();
+ int out = server.arg("out").toInt();
+ String S = "Refut : gpio =" + String(gpio) + " out =" + String(out);
+ if (gpio >= 0 && gpio <= 33 && out >= 0 && out <= 1) {
+ pinMode(gpio, OUTPUT);
+ digitalWrite(gpio, out);
+ S = "OK : gpio =" + String(gpio) + " out =" + String(out);
+ }
+ server.send(200, "text/html", S);
+}
+void handleAP_ScanWifi() {
+ server.send(200, "text/html", Liste_AP);
+}
+void Liste_WIFI() { //Doit ĂŞtre fait avant toute connection WIFI depuis biblio ESP32 3.0.1
+ WIFIbug = 0;
+ ComBug=0;
+ int n = 0;
+ WiFi.disconnect();
+ delay(100);
+ Serial.println("Scan start");
+ // WiFi.scanNetworks will return the number of networks found.
+ n = WiFi.scanNetworks();
+ Serial.println("Scan done");
+ Liste_AP = "";
+ if (n == 0) {
+ Serial.println("Pas de réseau Wifi trouvé");
+ } else {
+ Serial.print(n);
+ Serial.println(" réseaux trouvés");
+ Serial.println("Nr | SSID | RSSI | CH | Encryption");
+ for (int i = 0; i < n; ++i) {
+ // Print SSID and RSSI for each network found
+ Serial.printf("%2d", i + 1);
+ Serial.print(" | ");
+ Serial.printf("%-32.32s", WiFi.SSID(i).c_str());
+ Serial.print(" | ");
+ Serial.printf("%4d", WiFi.RSSI(i));
+ Serial.println();
+ Liste_AP += WiFi.SSID(i).c_str() + RS + WiFi.RSSI(i) + GS;
+ }
+ }
+ WiFi.scanDelete();
+}
+void handleAP_SetWifi() {
+ WIFIbug = 0;
+ ComBug =0;
+ Serial.println("Set Wifi");
+ String NewSsid = server.arg("ssid");
+ NewSsid.trim();
+ String NewPassword = server.arg("passe");
+ NewPassword.trim();
+ Serial.println(NewSsid);
+ Serial.println(NewPassword);
+ ssid = NewSsid;
+ password = NewPassword;
+ StockMessage("Wifi Begin : " + ssid);
+ WiFi.begin(ssid.c_str(), password.c_str());
+ unsigned long newstartMillis = millis();
+ while (WiFi.status() != WL_CONNECTED && (millis() - newstartMillis < 20000)) { // Attente connexion au Wifi
+ Serial.write('!');
+ Gestion_LEDs();
+ Serial.print(WiFi.status());
+ delay(300);
+ }
+ Serial.println();
+ String S = "";
+ if (WiFi.status() == WL_CONNECTED) {
+ Serial.print("IP address: ");
+ Serial.println(WiFi.localIP());
+ String IP = WiFi.localIP().toString();
+ S = "Ok" + RS;
+ S += "ESP 32 connecté avec succès au wifi : " + ssid + " avec l'adresse IP : " + IP;
+ S += "
Connectez vous au wifi : " + ssid;
+ S += "
Cliquez sur l'adresse : http://" + IP + "";
+ dhcpOn = 1;
+ EcritureEnROM();
+ } else {
+ S = "No" + RS + "ESP32 non connecté à :" + ssid + "
";
+ }
+ server.send(200, "text/html", S);
+ delay(1000);
+ ESP.restart();
+}
+
+
+void handleNotFound() { //Page Web pas trouvé
+ String message = "Fichier non trouvé\n\n";
+ message += "URI: ";
+ message += server.uri();
+ message += "\nMethod: ";
+ message += (server.method() == HTTP_GET) ? "GET" : "POST";
+ message += "\nArguments: ";
+ message += server.args();
+ message += "\n";
+ for (uint8_t i = 0; i < server.args(); i++) {
+ message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
+ }
+ server.send(404, "text/plain", message);
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino
new file mode 100644
index 0000000..26b9c55
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino
@@ -0,0 +1,1124 @@
+/*
+ PV Router / Routeur Solaire
+ ****************************************
+ Version V10.00
+
+ RMS=Routeur Multi Sources
+
+ Choix de 9 sources différentes pour lire la consommation électrique en entrée de maison
+ - lecture de la tension avec un transformateur et du courant avec une sonde ampèremétrique (UxI)
+ - lecture des données du Linky (Linky)
+ - module (JSY-MK-194T) intégrant une mesure de tension secteur et 2 sondes ampèmétriques (UxIx2)
+ - module (JSY-MK-333) pour une installation triphasé
+ - Lecture passerelle Enphase - Envoy-S metered (firmware V5 et V7)
+ - Lecture depuis un autre ESP qui comprend l'une des 4 sources citées plus haut
+ - Lecture avec Shelly Em
+ - Lecture compteur SmartG
+ - Lecture via MQTT
+
+ En option une mesure de température en interne (DS18B20), en externe ou via MQTT est possible.
+
+ Historique des versions
+ - V9.00_RMS
+ Stockage des températures avec une décimale
+ Simplification changement de nom de réseau WIFI
+ Choix mode Wifi avec ou sans veille
+ Sélection source de température
+ Source de puissance reçue via MQTT
+ Souscription MQTT à une température externe
+ Souscription MQTT pour forcer On ou Off les actionneurs.
+ - V9.01_RMS fonctionne avec la bibliothèque ESP32 Version 2.0.17
+ Validation Pva_valide pour les Linky en CACSI
+ - V9.02_RMS fonctionne avec la bibliothèque ES¨P32 V 3.01 .
+ Suite au passage de la bibliothèque ESP32 en Version 3.01 importants changement pour le routeur sur le WIFI, les Timers, Le Watchdog et la partition mémoire FLASH.
+ Attention à ne pas utiliser la bibliothèque ESP32 en Version 3.00, elle est bugée et génère 20% de plus de code.
+ Filtrage des températures pour tolérer une perte éventuelle de mesure
+ - V9.03_RMS
+ Suite au changement de bibliothèque ESP32 en V3.0.1, le scan réseau pour un changement de nom de WIFI ne fonctionnait plus. Scan fait maintenant au boot.
+ - V10.00
+ OTA par le Web directement en complément de l'Arduino IDE
+ Modification des calculs de puissance en UxIx3 pour avoir une représentation similaire au Linky (Merci PhDV61)
+ Modification de la surveillance Watchdog
+
+
+ Les détails sont disponibles sur / Details are available here:
+ https://f1atb.fr Section Domotique / Home Automation
+
+ F1ATB Juin 2024
+
+ GNU Affero General Public License (AGPL) / AGPL-3.0-or-later
+
+
+
+*/
+#define Version "10.00"
+#define HOSTNAME "RMS-ESP32-"
+#define CLE_Rom_Init 912567899 //Valeur pour tester si ROM vierge ou pas. Un changement de valeur remet à zéro toutes les données. / Value to test whether blank ROM or not.
+
+
+//Librairies
+#include
+#include
+#include
+#include
+#include //Modification On The Air
+#include //Pour un Watchdog
+#include //Librairie pour la gestion Mqtt
+#include "EEPROM.h" //Librairie pour le stockage en EEPROM historique quotidien
+#include "esp_sntp.h"
+#include "OneWire.h"
+#include "DallasTemperature.h"
+#include "UrlEncode.h"
+#include
+#include
+
+//Program routines
+#include "pageHtmlBrute.h"
+#include "pageHtmlMain.h"
+#include "pageHtmlConnect.h"
+#include "pageHtmlPara.h"
+#include "pageHtmlActions.h"
+#include "pageHtmlOTA.h"
+#include "Actions.h"
+
+
+//Watchdog de 180 secondes. Le systeme se Reset si pas de dialoque avec le LINKY ou JSY-MK-194T/333 ou Enphase-Envoy pendant 180s
+//Watchdog for 180 seconds. The system resets if no dialogue with the Linky or JSY-MK-194T/333 or Enphase-Envoy for 180s
+#define WDT_TIMEOUT 180
+
+//PINS - GPIO
+
+#define AnalogIn0 35 //Pour Routeur Uxi
+#define AnalogIn1 32
+#define AnalogIn2 33 //Note: si GPIO 33 non disponible sur la carte ESP32, utilisez la 34. If GPIO 33 not available on the board replace by GPIO 34
+#define RXD2_1 16 //Pour Routeur Linky ou UxIx2 (sur carte ESP32 simple): Couple RXD2=26 et TXD2=27 . Pour carte ESP32 4 relais : Couple RXD2=17 et TXD2=27
+#define TXD2_1 17
+#define RXD2_2 26 //Pour Routeur Linky ou UxIx2 (sur carte ESP32 simple): Couple RXD2=26 et TXD2=27 . Pour carte ESP32 4 relais : Couple RXD2=17 et TXD2=27
+#define TXD2_2 27
+#define SER_BUF_SIZE 4096
+#define LedYellow 18
+#define LedGreen 19
+#define pulseTriac_1 4
+#define zeroCross_1 5
+#define pulseTriac_2 22
+#define zeroCross_2 23
+#define pinTemp 13 //Capteur température
+
+
+//Nombre Actions Max
+#define LesActionsLength 10
+//VARIABLES
+const char *ap_default_ssid; // Mode Access point IP: 192.168.4.1
+const char *ap_default_psk = NULL; // Pas de mot de passe en AP,
+
+//Paramètres pour le stockage en ROM apres les données du RMS
+unsigned long Cle_ROM;
+
+String ssid = "";
+String password = "";
+String Source = "UxI";
+String Source_data = "UxI";
+byte dhcpOn = 1;
+unsigned long IP_Fixe = 0;
+unsigned long Gateway = 0;
+unsigned long masque = 4294967040;
+unsigned long dns = 0;
+unsigned long RMSextIP = 0;
+unsigned int MQTTRepet = 0;
+unsigned long MQTTIP = 0;
+unsigned int MQTTPort = 1883;
+String MQTTUser = "User";
+String MQTTPwd = "password";
+String MQTTPrefix = "homeassistant"; // prefix obligatoire pour l'auto-discovery entre HA et Core-Mosquitto (par défaut c'est homeassistant)
+String MQTTdeviceName = "routeur_rms";
+String TopicP = "PuissanceMaison";
+String TopicT = "TemperatureMQTT";
+unsigned long IPtemp = 0;
+byte subMQTT = 0;
+String nomRouteur = "Routeur - RMS";
+String nomSondeFixe = "Données seconde sonde";
+String nomSondeMobile = "Données Maison";
+String nomTemperature = "Température";
+byte WifiSleep = 1;
+byte pSerial = 2; //Choix Pin port serie
+byte pTriac = 2; //Choix Pin Triac
+String Source_Temp = "tempNo";
+String GS = String((char)29); //Group Separator
+String RS = String((char)30); //Record Separator
+String MessageH[10];
+int idxMessage = 0;
+int P_cent_EEPROM;
+int cptLEDyellow = 0;
+int cptLEDgreen = 0;
+
+unsigned int CalibU = 1000; //Calibration Routeur UxI
+unsigned int CalibI = 1000;
+int value0;
+int volt[100];
+int amp[100];
+float KV = 0.2083; //Calibration coefficient for the voltage. Value for CalibU=1000 at startup
+float KI = 0.0642; //Calibration coefficient for the current. Value for CalibI=1000 at startup
+float kV = 0.2083; //Calibration coefficient for the voltage. Corrected value
+float kI = 0.0642; //Calibration coefficient for the current. Corrected value
+float voltM[100]; //Voltage Mean value
+float ampM[100];
+
+bool EnergieActiveValide = false;
+long EAS_T_J0 = 0;
+long EAI_T_J0 = 0;
+long EAS_M_J0 = 0; //Debut du jour energie active
+long EAI_M_J0 = 0;
+
+
+int adr_debut_para = 0; //Adresses Para après le Wifi
+
+
+//Paramètres électriques
+float Tension_T, Intensite_T, PowerFactor_T, Frequence;
+float Tension_M, Intensite_M, PowerFactor_M;
+long Energie_T_Soutiree = 0;
+long Energie_T_Injectee = 0;
+long Energie_M_Soutiree = 0;
+long Energie_M_Injectee = 0;
+long EnergieJour_T_Injectee = 0;
+long EnergieJour_M_Injectee = 0;
+long EnergieJour_T_Soutiree = 0;
+long EnergieJour_M_Soutiree = 0;
+int PuissanceS_T, PuissanceS_M, PuissanceI_T, PuissanceI_M;
+int PVAS_T, PVAS_M, PVAI_T, PVAI_M;
+float PuissanceS_T_inst, PuissanceS_M_inst, PuissanceI_T_inst, PuissanceI_M_inst;
+float PVAS_T_inst, PVAS_M_inst, PVAI_T_inst, PVAI_M_inst;
+float Puissance_T_moy, Puissance_M_moy;
+float PVA_T_moy, PVA_M_moy;
+float EASfloat = 0;
+float EAIfloat = 0;
+int PactConso_M, PactProd;
+int tabPw_Maison_5mn[600]; //Puissance Active:Soutiré-Injecté toutes les 5mn
+int tabPw_Triac_5mn[600];
+int tabTemperature_5mn[600];
+int tabPw_Maison_2s[300]; //Puissance Active: toutes les 2s
+int tabPw_Triac_2s[300]; //Puissance Triac: toutes les 2s
+int tabPva_Maison_2s[300]; //Puissance Active: toutes les 2s
+int tabPva_Triac_2s[300];
+int tabPulseSinusOn[101];
+int tabPulseSinusTotal[101];
+int tab_histo_ouverture[LesActionsLength][600];
+int IdxStock2s = 0;
+int IdxStockPW = 0;
+float PmaxReseau = 36000; //Puissance Max pour eviter des débordements
+bool LissageLong = false;
+bool Pva_valide = false;
+int RXD2, TXD2; //Port serie
+int pulseTriac, zeroCross;
+
+//Parameters for JSY-MK-194T module
+byte ByteArray[130];
+long LesDatas[14];
+int Sens_1, Sens_2;
+
+
+//Parameters for JSY-MK-333 module triphasé
+String MK333_dataBrute = "";
+// ajout PhDV61 compteur d'énergie quotidienne soutirée et injectée comme calculées par le Linky
+float Energie_jour_Soutiree = 0;
+float Energie_jour_Injectee = 0;
+long Temps_precedent = 0; // mesure précise du temps entre deux appels au JSY-MK-333
+
+//Parameters for Linky
+bool LFon = false;
+bool EASTvalid = false;
+bool EAITvalid = false;
+volatile int IdxDataRawLinky = 0;
+volatile int IdxBufDecodLinky = 0;
+volatile char DataRawLinky[10000]; //Buffer entrée données Linky
+float moyPWS = 0;
+float moyPWI = 0;
+float moyPVAS = 0;
+float moyPVAI = 0;
+float COSphiS = 1;
+float COSphiI = 1;
+long TlastEASTvalide = 0;
+long TlastEAITvalide = 0;
+String LTARF = ""; //Option tarifaire EDF
+String STGE = ""; //Status Tempo uniquement EDF
+
+//Paramètres for Enphase-Envoy-Smetered
+String TokenEnphase = "";
+String EnphaseUser = "";
+String EnphasePwd = "";
+String EnphaseSerial = "0"; //Sert égalemnet au Shelly comme numéro de voie
+String JsonToken = "";
+String Session_id = "";
+long LastwhDlvdCum = 0; //Dernière valeur cumul Wh Soutire-injecté.
+float EMI_Wh = 0; //Energie entrée Maison Injecté Wh
+float EMS_Wh = 0; //Energie entrée Maison Soutirée Wh
+
+//Paramètres for SmartGateways
+String SG_dataBrute = "";
+
+//Paramètres for Shelly Em
+String ShEm_dataBrute = "";
+int ShEm_comptage_appels = 0;
+float PwMoy2 = 0; //Moyenne voie secondsaire
+float pfMoy2 = 1; //pf Voie secondaire
+
+//Paramètres pour puissance via MQTT
+String P_MQTT_Brute = "";
+float PwMQTT = 0;
+float PvaMQTT = 0;
+float PfMQTT = 1;
+
+//Paramètres pour EDF
+String DateEDF = ""; //an-mois-jour
+byte TempoEDFon = 0;
+int LastHeureEDF = -1;
+int LTARFbin = 0; //Code binaire des tarifs
+
+
+
+//Actions
+Action LesActions[LesActionsLength]; //Liste des actions
+volatile int NbActions = 0;
+
+
+
+//Internal Timers
+unsigned long startMillis;
+unsigned long previousWifiMillis;
+unsigned long previousHistoryMillis;
+unsigned long previousWsMillis;
+unsigned long previousWiMillis;
+unsigned long LastRMS_Millis;
+unsigned long previousTimer2sMillis;
+unsigned long previousOverProdMillis;
+unsigned long previousLEDsMillis;
+unsigned long previousActionMillis;
+unsigned long previousTempMillis;
+unsigned long previousLoop;
+unsigned long previousETX;
+unsigned long PeriodeProgMillis = 1000;
+unsigned long T0_seconde = 0;
+unsigned long T_On_seconde = 0;
+float previousLoopMin = 1000;
+float previousLoopMax = 0;
+float previousLoopMoy = 0;
+unsigned long previousTimeRMS;
+float previousTimeRMSMin = 1000;
+float previousTimeRMSMax = 0;
+float previousTimeRMSMoy = 0;
+unsigned long previousMQTTenvoiMillis;
+unsigned long previousMQTTMillis;
+unsigned long LastPwMQTTMillis = 0;
+
+//Actions et Triac(action 0)
+float RetardF[LesActionsLength]; //Floating value of retard
+//Variables in RAM for interruptions
+volatile unsigned long lastIT = 0;
+volatile int IT10ms = 0; //Interruption avant deglitch
+volatile int IT10ms_in = 0; //Interruption apres deglitch
+volatile int ITmode = 0; //IT exerne Triac ou interne
+hw_timer_t *timer = NULL;
+hw_timer_t *timer10ms = NULL;
+
+
+volatile int Retard[LesActionsLength];
+volatile int Actif[LesActionsLength];
+volatile int PulseOn[LesActionsLength];
+volatile int PulseTotal[LesActionsLength];
+volatile int PulseComptage[LesActionsLength];
+volatile int Gpio[LesActionsLength];
+volatile int OutOn[LesActionsLength];
+volatile int OutOff[LesActionsLength];
+
+WebServer server(80); // Simple Web Server on port 80
+
+//Port Serie 2 - Remplace Serial2 qui bug
+HardwareSerial MySerial(2);
+
+// Heure et Date
+#define MAX_SIZE_T 80
+const char *ntpServer1 = "fr.pool.ntp.org";
+const char *ntpServer2 = "time.nist.gov";
+String DATE = "";
+String DateCeJour = "";
+bool DATEvalid = false;
+int HeureCouranteDeci = 0;
+int idxPromDuJour = 0;
+
+//Température Capteur DS18B20
+OneWire oneWire(pinTemp);
+DallasTemperature ds18b20(&oneWire);
+float temperature = -127; // La valeur vaut -127 quand la sonde DS18B20 n'est pas présente
+bool ds18b20_Init = false;
+int TemperatureValide = 0;
+
+
+//MQTT
+WiFiClient MqttClient;
+PubSubClient clientMQTT(MqttClient);
+
+//WIFI
+int WIFIbug = 0;
+int ComBug = 0;
+WiFiClientSecure clientSecu;
+WiFiClientSecure clientSecuEDF;
+String Liste_AP = "";
+
+
+//Multicoeur - Processeur 0 - Collecte données RMS local ou distant
+TaskHandle_t Task1;
+esp_err_t ESP32_ERROR;
+bool FirstLoop0 = false;
+
+//Interruptions, Current Zero Crossing from Triac device and Internal Timer
+//*************************************************************************
+void IRAM_ATTR onTimer10ms() { //Interruption interne toutes 10ms
+ ITmode = ITmode - 1;
+ if (ITmode < -5) ITmode = -5;
+ if (ITmode < 0) GestionIT_10ms(); //IT non synchrone avec le secteur . Horloge interne
+}
+
+
+// Interruption du Triac Signal Zc, toutes les 10ms
+void IRAM_ATTR currentNull() {
+ IT10ms = IT10ms + 1;
+ if ((millis() - lastIT) > 2) { // to avoid glitch detection during 2ms
+ ITmode = ITmode + 2;
+ if (ITmode > 5) ITmode = 5;
+ IT10ms_in = IT10ms_in + 1;
+ lastIT = millis();
+ if (ITmode > 0) GestionIT_10ms(); //IT synchrone avec le secteur signal Zc
+ }
+}
+
+
+void GestionIT_10ms() {
+ for (int i = 0; i < NbActions; i++) {
+ switch (Actif[i]) { //valeur en RAM
+ case 0: //Inactif
+
+ break;
+ case 1: //Decoupe Sinus uniquement pour Triac
+ if (i == 0) {
+ PulseComptage[0] = 0;
+ digitalWrite(pulseTriac, LOW); //Stop DĂ©coupe Triac
+ }
+ break;
+ default: // Multi Sinus ou Train de sinus
+ if (Gpio[i] > 0) { //Gpio valide
+ if (PulseComptage[i] < PulseOn[i]) {
+ digitalWrite(Gpio[i], OutOn[i]);
+ } else {
+ digitalWrite(Gpio[i], OutOff[i]); //Stop
+ }
+ PulseComptage[i] = PulseComptage[i] + 1;
+ if (PulseComptage[i] >= PulseTotal[i]) {
+ PulseComptage[i] = 0;
+ }
+ }
+ break;
+ }
+ }
+}
+
+// Interruption Timer interne toutes les 100 micro secondes
+void IRAM_ATTR onTimer() { //Interruption every 100 micro second
+ if (Actif[0] == 1) { // DĂ©coupe Sinus
+ PulseComptage[0] = PulseComptage[0] + 1;
+ if (PulseComptage[0] > Retard[0] && Retard[0] < 98 && ITmode > 0) { //100 steps in 10 ms
+ digitalWrite(pulseTriac, HIGH); //Activate Triac
+ } else {
+ digitalWrite(pulseTriac, LOW); //Stop Triac
+ }
+ }
+}
+
+// SETUP
+//*******
+void setup() {
+ startMillis = millis();
+ previousLEDsMillis = startMillis;
+
+ //Pin initialisation
+ pinMode(LedYellow, OUTPUT);
+ pinMode(LedGreen, OUTPUT);
+ digitalWrite(LedYellow, LOW);
+ digitalWrite(LedGreen, LOW);
+
+ //Ports SĂ©rie ESP
+ Serial.begin(115200);
+ Serial.println("Booting");
+
+
+ //Watchdog initialisation
+ // Initialisation de la structure de configuration pour la WDT
+ esp_task_wdt_config_t wdt_config = {
+ .timeout_ms = WDT_TIMEOUT * 1000, // Convertir le temps en millisecondes
+ .idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // Bitmask of all cores, https://github.com/espressif/esp-idf/blob/v5.2.2/examples/system/task_watchdog/main/task_watchdog_example_main.c
+ .trigger_panic = true // Enable panic to restart ESP32
+ };
+ // Initialisation de la WDT avec la structure de configuration
+ ESP32_ERROR = esp_task_wdt_init(&wdt_config);
+ StockMessage("Dernier Reset : " + String(esp_err_to_name(ESP32_ERROR)));
+
+
+
+ for (int i = 0; i < LesActionsLength; i++) {
+ LesActions[i] = Action(i); //Creation objets
+ PulseOn[i] = 0; //1/2 sinus
+ PulseTotal[i] = 100;
+ PulseComptage[i] = 0;
+ Retard[i] = 100;
+ RetardF[i] = 100;
+ OutOn[i] = 1;
+ OutOff[i] = 0;
+ Gpio[i] = -1;
+ }
+
+
+ //Tableau Longueur Pulse et Longueur Trame pour Multi-Sinus de 0 Ă 100%
+ float erreur;
+ float vrai;
+ float target;
+ for (int I = 0; I < 101; I++) {
+ tabPulseSinusTotal[I] = -1;
+ tabPulseSinusOn[I] = -1;
+ target = float(I) / 100.0;
+ for (int T = 20; T < 101; T++) {
+ for (int N = 0; N <= T; N++) {
+ if (T % 2 == 1 || N % 2 == 0) { // Valeurs impair du total ou pulses pairs pour Ă©viter courant continu
+ vrai = float(N) / float(T);
+ erreur = abs(vrai - target);
+ if (erreur < 0.004) {
+ tabPulseSinusTotal[I] = T;
+ tabPulseSinusOn[I] = N;
+ N = 101;
+ T = 101;
+ }
+ }
+ }
+ }
+ }
+
+ init_puissance();
+ //Liste Wifi Ă faire avant connexion Ă un AP. Necessaire depuis biblio ESP32 3.0.1
+ WiFi.mode(WIFI_STA);
+ WiFi.disconnect();
+ Liste_WIFI();
+
+
+ Serial.print("Version : ");
+ Serial.println(Version);
+ // Configure WIFI
+ // **************
+ String hostname(HOSTNAME);
+ uint32_t chipId = 0;
+ for (int i = 0; i < 17; i = i + 8) {
+ chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
+ }
+ hostname += String(chipId); //Add chip ID to hostname
+ WiFi.hostname(hostname);
+ Serial.println(hostname);
+ ap_default_ssid = (const char *)hostname.c_str();
+ // Check WiFi connection
+ // ... check mode
+ if (WiFi.getMode() != WIFI_STA) {
+ WiFi.mode(WIFI_STA);
+ delay(10);
+ }
+
+ INIT_EEPROM();
+ //Lecture Clé pour identifier si la ROM a déjà été initialisée
+ Cle_ROM = CLE_Rom_Init;
+ unsigned long Rcle = LectureCle();
+ Serial.println("cle : " + String(Rcle));
+ if (Rcle == Cle_ROM) { // Programme déjà executé
+ LectureEnROM();
+ LectureConsoMatinJour();
+ InitGpioActions();
+ } else {
+ RAZ_Histo_Conso();
+ }
+
+ //Triac init
+ if (pTriac > 0) {
+ pulseTriac = pulseTriac_2;
+ zeroCross = zeroCross_2;
+ if (pTriac == 1) {
+ pulseTriac = pulseTriac_1;
+ zeroCross = zeroCross_1;
+ }
+ pinMode(zeroCross, INPUT_PULLDOWN);
+ pinMode(pulseTriac, OUTPUT);
+ digitalWrite(pulseTriac, LOW); //Stop Triac
+ } else {
+ Actif[0] = 0;
+ LesActions[0].Actif = 0;
+ }
+ Gpio[0] = pulseTriac;
+ LesActions[0].Gpio = pulseTriac;
+
+ //Heure / Hour . A Mettre en priorité avant WIFI (exemple ESP32 Simple Time)
+ //External timer to obtain the Hour and reset Watt Hour every day at 0h
+ sntp_set_time_sync_notification_cb(time_sync_notification);
+ //sntp_servermode_dhcp(1); Déprecié
+ esp_sntp_servermode_dhcp(true); //Option
+ configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer1, ntpServer2); //Voir Time-Zone: https://sites.google.com/a/usapiens.com/opnode/time-zones
+
+
+
+ //WIFI
+ Serial.println("SSID:" + ssid);
+ Serial.println("Pass:" + password);
+ if (ssid.length() > 0) {
+ if (dhcpOn == 0) { //Static IP
+ byte arr[4];
+ arr[0] = IP_Fixe & 0xFF; // 0x78
+ arr[1] = (IP_Fixe >> 8) & 0xFF; // 0x56
+ arr[2] = (IP_Fixe >> 16) & 0xFF; // 0x34
+ arr[3] = (IP_Fixe >> 24) & 0xFF; // 0x12
+ // Set your Static IP address
+ IPAddress local_IP(arr[3], arr[2], arr[1], arr[0]);
+ // Set your Gateway IP address
+ arr[0] = Gateway & 0xFF; // 0x78
+ arr[1] = (Gateway >> 8) & 0xFF; // 0x56
+ arr[2] = (Gateway >> 16) & 0xFF; // 0x34
+ arr[3] = (Gateway >> 24) & 0xFF; // 0x12
+ IPAddress gateway(arr[3], arr[2], arr[1], arr[0]);
+ // Set your masque/subnet IP address
+ arr[0] = masque & 0xFF;
+ arr[1] = (masque >> 8) & 0xFF;
+ arr[2] = (masque >> 16) & 0xFF;
+ arr[3] = (masque >> 24) & 0xFF;
+ IPAddress subnet(arr[3], arr[2], arr[1], arr[0]);
+ // Set your DNS IP address
+ arr[0] = dns & 0xFF;
+ arr[1] = (dns >> 8) & 0xFF;
+ arr[2] = (dns >> 16) & 0xFF;
+ arr[3] = (dns >> 24) & 0xFF;
+ IPAddress primaryDNS(arr[3], arr[2], arr[1], arr[0]); //optional
+ IPAddress secondaryDNS(8, 8, 4, 4); //optional
+ if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
+ Serial.println("WIFI STA Failed to configure");
+ }
+ }
+ StockMessage("Wifi Begin : " + ssid);
+ WiFi.begin(ssid.c_str(), password.c_str());
+ WiFi.setSleep(WifiSleep);
+ while (WiFi.status() != WL_CONNECTED && (millis() - startMillis < 20000)) { // Attente connexion au Wifi
+ Serial.write('.');
+ Gestion_LEDs();
+ Serial.print(WiFi.status());
+ delay(300);
+ }
+ Serial.println();
+ }
+ if (WiFi.status() == WL_CONNECTED) {
+ StockMessage("Connected IP address: " + WiFi.localIP().toString() + " or " + hostname + ".local");
+ } else {
+ StockMessage("Can not connect to WiFi station. Go into AP mode and STA mode.");
+ // Go into software AP and STA modes.
+ //WiFi.disconnect();
+ delay(100);
+ WiFi.mode(WIFI_AP_STA);
+ delay(10);
+ WiFi.softAP(ap_default_ssid, ap_default_psk);
+ Serial.print("Access Point Mode. IP address: ");
+ Serial.println(WiFi.softAPIP());
+ }
+
+
+ Init_Server();
+
+
+ // Modification du programme par le Wifi - OTA(On The Air)
+ //***************************************************
+ ArduinoOTA.setHostname((const char *)hostname.c_str());
+ ArduinoOTA.begin(); //Mandatory
+
+
+
+ //Adaptation Ă la Source
+ Serial.println("Source : " + Source);
+
+ if (Source == "UxI") {
+ Setup_UxI();
+ }
+
+ if (Source == "Enphase") {
+ Setup_Enphase();
+ }
+
+
+ if (Source == "Pmqtt") {
+ GestionMQTT();
+ }
+
+ //Port SĂ©rie si besoin
+ if (pSerial > 0) {
+ RXD2 = RXD2_2;
+ TXD2 = TXD2_2;
+ if (pSerial == 1) {
+ RXD2 = RXD2_1;
+ TXD2 = TXD2_1;
+ }
+ if (Source == "UxIx2") {
+ Setup_UxIx2();
+ }
+ if (Source == "UxIx3") {
+ Setup_JSY333();
+ }
+ if (Source == "Linky") {
+ Setup_Linky();
+ }
+ }
+
+ if (Source == "Ext") {
+ } else {
+ Source_data = Source;
+ }
+
+
+
+
+ xTaskCreatePinnedToCore( //Préparation Tâche Multi Coeur
+ Task_LectureRMS, /* Task function. */
+ "Task_LectureRMS", /* name of task. */
+ 10000, /* Stack size of task */
+ NULL, /* parameter of the task */
+ 10, /* priority of the task */
+ &Task1, /* Task handle to keep track of created task */
+ 0); /* pin task to core 0 */
+
+
+ if (pTriac > 0) {
+ //Interruptions du Triac et Timer interne
+ attachInterrupt(zeroCross, currentNull, RISING);
+ }
+
+ //Hardware timer 100uS
+ timer = timerBegin(1000000); //Clock 1MHz
+ timerAttachInterrupt(timer, &onTimer);
+ timerAlarm(timer, 100, true, 0); //Interrupt every 100 microsecond
+
+ //Hardware timer 10ms
+ timer10ms = timerBegin(1000000); //Clock 1MHz
+ timerAttachInterrupt(timer10ms, &onTimer10ms);
+ timerAlarm(timer10ms, 10000, true, 0); //Interrupt every 10ms
+
+
+ //Timers
+ previousWifiMillis = millis() - 25000;
+ previousHistoryMillis = millis() - 290000;
+ previousTimer2sMillis = millis();
+ previousLoop = millis();
+ previousTimeRMS = millis();
+ previousMQTTenvoiMillis = millis();
+ previousMQTTMillis = millis();
+ previousETX = millis();
+ previousOverProdMillis = millis();
+ LastRMS_Millis = millis();
+ previousActionMillis = millis();
+ previousTempMillis = millis() - 118000;
+}
+
+/* **********************
+ * ****************** *
+ * * Tâches Coeur 0 * *
+ * ****************** *
+ **********************
+*/
+
+void Task_LectureRMS(void *pvParameters) {
+ esp_task_wdt_add(NULL); //add current thread to WDT watch
+ esp_task_wdt_reset();
+ for (;;) {
+ if (!FirstLoop0) {
+ Serial.println("FirstLoop0");
+ FirstLoop0 = true;
+ ComOK();
+ }
+ unsigned long tps = millis();
+ float deltaT = float(tps - previousTimeRMS);
+ previousTimeRMS = tps;
+ previousTimeRMSMin = min(previousTimeRMSMin, deltaT);
+ previousTimeRMSMin = previousTimeRMSMin + 0.002;
+ previousTimeRMSMax = max(previousTimeRMSMax, deltaT);
+ previousTimeRMSMax = previousTimeRMSMax * 0.999;
+ previousTimeRMSMoy = deltaT * 0.01 + previousTimeRMSMoy * 0.99;
+ previousTimeRMSMin = min(previousTimeRMSMin, previousTimeRMSMoy);
+ previousTimeRMSMax = max(previousTimeRMSMax, previousTimeRMSMoy);
+
+
+ //Recupération des données RMS
+ //******************************
+ if (tps - LastRMS_Millis > PeriodeProgMillis) { //Attention delicat pour eviter pb overflow
+ LastRMS_Millis = tps;
+ unsigned long ralenti = long(PuissanceS_M / 10); // On peut ralentir Ă©change sur Wifi si grosse puissance en cours
+ if (Source == "UxI") {
+ LectureUxI();
+ PeriodeProgMillis = 40;
+ }
+ if (pSerial > 0) {
+ if (Source == "UxIx2") {
+ LectureUxIx2();
+ PeriodeProgMillis = 400;
+ }
+ if (Source == "UxIx3") {
+ Lecture_JSY333();
+ PeriodeProgMillis = 600;
+ }
+ if (Source == "Linky") {
+ LectureLinky();
+ PeriodeProgMillis = 2;
+ }
+ }
+ if (Source == "Enphase") {
+ LectureEnphase();
+ LastRMS_Millis = millis();
+ PeriodeProgMillis = 600 + ralenti; //On s'adapte à la vitesse réponse Envoy-S metered
+ }
+ if (Source == "SmartG") {
+ LectureSmartG();
+ LastRMS_Millis = millis();
+ PeriodeProgMillis = 200 + ralenti; //On s'adapte à la vitesse réponse SmartGateways
+ }
+ if (Source == "ShellyEm") {
+ LectureShellyEm();
+ LastRMS_Millis = millis();
+ PeriodeProgMillis = 200 + ralenti; //On s'adapte à la vitesse réponse ShellyEm
+ }
+
+ if (Source == "Ext") {
+ CallESP32_Externe();
+ LastRMS_Millis =
+ PeriodeProgMillis = 200 + ralenti; //Après pour ne pas surchargé Wifi
+ }
+ if (Source == "Pmqtt") {
+ PeriodeProgMillis = 600;
+ LastRMS_Millis = millis();
+ UpdatePmqtt();
+ }
+ }
+ delay(2);
+ }
+}
+
+
+
+
+/* **********************
+ * ****************** *
+ * * Tâches Coeur 1 * *
+ * ****************** *
+ **********************
+*/
+void loop() {
+ //Estimation charge coeur
+ unsigned long tps = millis();
+ float deltaT = float(tps - previousLoop);
+ previousLoop = tps;
+ previousLoopMin = min(previousLoopMin, deltaT);
+ previousLoopMin = previousLoopMin + 0.002;
+ previousLoopMax = max(previousLoopMax, deltaT);
+ previousLoopMax = previousLoopMax * 0.999;
+ previousLoopMoy = deltaT * 0.01 + previousLoopMoy * 0.99;
+ previousLoopMin = min(previousLoopMin, previousLoopMoy);
+ previousLoopMax = max(previousLoopMax, previousLoopMoy);
+ //Gestion des serveurs
+ //********************
+ ArduinoOTA.handle();
+ server.handleClient();
+
+ //Archivage et envois des mesures périodiquement
+ //**********************************************
+ if (EnergieActiveValide) {
+ if (tps - previousHistoryMillis >= 300000) { //Historique consommation par pas de 5mn
+ previousHistoryMillis = tps;
+ tabPw_Maison_5mn[IdxStockPW] = PuissanceS_M - PuissanceI_M;
+ tabPw_Triac_5mn[IdxStockPW] = PuissanceS_T - PuissanceI_T;
+ if (temperature > -20) {
+ tabTemperature_5mn[IdxStockPW] = int(temperature * 10);
+ } else {
+ tabTemperature_5mn[IdxStockPW] = 0;
+ }
+
+
+ for (int i = 0; i < NbActions; i++) {
+ if (Actif[i] > 0) {
+ tab_histo_ouverture[i][IdxStockPW] = 100 - Retard[i];
+ } else {
+ tab_histo_ouverture[i][IdxStockPW] = 0;
+ }
+ }
+ IdxStockPW = (IdxStockPW + 1) % 600;
+ }
+
+
+ if (tps - previousTimer2sMillis >= 2000) {
+ previousTimer2sMillis = tps;
+ tabPw_Maison_2s[IdxStock2s] = PuissanceS_M - PuissanceI_M;
+ tabPw_Triac_2s[IdxStock2s] = PuissanceS_T - PuissanceI_T;
+ tabPva_Maison_2s[IdxStock2s] = PVAS_M - PVAI_M;
+ tabPva_Triac_2s[IdxStock2s] = PVAS_T - PVAI_T;
+ IdxStock2s = (IdxStock2s + 1) % 300;
+ JourHeureChange();
+ EnergieQuotidienne();
+ }
+
+ if (tps - previousOverProdMillis >= 200) {
+ previousOverProdMillis = tps;
+ GestionOverproduction();
+ }
+ }
+ if (tps - previousMQTTMillis > 200) {
+ previousMQTTMillis = tps;
+ GestionMQTT();
+ }
+ if (tps - previousLEDsMillis >= 50) {
+ previousLEDsMillis = tps;
+ Gestion_LEDs();
+ }
+ //Actions forcées et température
+ if (tps - previousActionMillis > 60000) {
+ previousActionMillis = tps;
+
+ for (int i = 0; i < NbActions; i++) {
+ if (LesActions[i].tOnOff > 0) LesActions[i].tOnOff -= 1;
+ if (LesActions[i].tOnOff < 0) LesActions[i].tOnOff += 1;
+ }
+ }
+ if (tps - previousTempMillis > 120001) {
+ previousTempMillis = tps;
+ //Temperature
+ LectureTemperature();
+ }
+ //VĂ©rification du WIFI
+ //********************
+ if (tps - previousWifiMillis > 30000) { //Test présence WIFI toutes les 30s et autres
+ previousWifiMillis = tps;
+ if (WiFi.getMode() == WIFI_STA) {
+ if (WiFi.waitForConnectResult(10000) != WL_CONNECTED) {
+ StockMessage("WIFI Connection Failed! #" + String(WIFIbug) + "ComBug #" + String(ComBug));
+ WIFIbug++;
+ if (WIFIbug > 4) {
+ ESP.restart();
+ }
+ } else {
+ WIFIbug = 0;
+ }
+
+
+ Serial.print("Niveau Signal WIFI:");
+ Serial.println(WiFi.RSSI());
+ Serial.print("IP address_: ");
+ Serial.println(WiFi.localIP());
+ Serial.print("WIFIbug : #");
+ Serial.println(WIFIbug);
+ Serial.print("ComBug : #");
+ Serial.println(ComBug);
+ Serial.println("Charge Lecture RMS (coeur 0) en ms - Min : " + String(int(previousTimeRMSMin)) + " Moy : " + String(int(previousTimeRMSMoy)) + " Max : " + String(int(previousTimeRMSMax)));
+ Serial.println("Charge Boucle générale (coeur 1) en ms - Min : " + String(int(previousLoopMin)) + " Moy : " + String(int(previousLoopMoy)) + " Max : " + String(int(previousLoopMax)));
+
+ int T = int(millis() / 1000);
+ float DureeOn = float(T) / 3600;
+ Serial.println("ESP32 ON depuis : " + String(DureeOn) + " heures");
+
+ JourHeureChange();
+ Call_EDF_data();
+ int Ltarf = 0; //Code binaire Tarif
+ if (LTARF.indexOf("PLEINE") >= 0) Ltarf += 1;
+ if (LTARF.indexOf("CREUSE") >= 0) Ltarf += 2;
+ if (LTARF.indexOf("BLEU") >= 0) Ltarf += 4;
+ if (LTARF.indexOf("BLANC") >= 0) Ltarf += 8;
+ if (LTARF.indexOf("ROUGE") >= 0) Ltarf += 16;
+ LTARFbin = Ltarf;
+ //Test pulse Zc Triac
+ if (ITmode < 0 && pTriac > 0) StockMessage("Erreur : pas de signal Zc du gradateur/Triac");
+ } else {
+ Serial.print("Access Point Mode. IP address: ");
+ Serial.println(WiFi.softAPIP());
+ }
+ }
+ if ((tps - startMillis) > 240000 && WiFi.getMode() != WIFI_STA) { //Connecté en Access Point depuis 4mn. Pas normal
+ Serial.println("Pas connecté en WiFi mode Station. Redémarrage");
+ delay(5000);
+ ESP.restart();
+ }
+}
+
+// ************
+// * ACTIONS *
+// ************
+void GestionOverproduction() {
+ float SeuilPw;
+ float MaxTriacPw;
+ float GainBoucle;
+ int Type_En_Cours = 0;
+ bool lissage = false;
+ //Puissance est la puissance en entrée de maison. >0 si soutire. <0 si injecte
+ //Cas du Triac. Action 0
+ float Puissance = float(PuissanceS_M - PuissanceI_M);
+ if (NbActions == 0) LissageLong = true; //Cas d'un capteur seul et actions déporté sur autre ESP
+ for (int i = 0; i < NbActions; i++) {
+ Actif[i] = LesActions[i].Actif; //0=Inactif,1=Decoupe ou On/Off, 2=Multi, 3= Train
+ if (Actif[i] >= 2) lissage = true; //En RAM
+ Type_En_Cours = LesActions[i].TypeEnCours(HeureCouranteDeci, temperature, LTARFbin); //0=NO,1=OFF,2=ON,3=PW,4=Triac
+ if (Actif[i] > 0 && Type_En_Cours > 1 && DATEvalid) { // On ne traite plus le NO
+ if (Type_En_Cours == 2) {
+ RetardF[i] = 0;
+ } else { // 3 ou 4
+ SeuilPw = float(LesActions[i].Valmin(HeureCouranteDeci));
+ MaxTriacPw = float(LesActions[i].Valmax(HeureCouranteDeci));
+ GainBoucle = float(LesActions[i].Reactivite); //Valeur stockée dans Port
+ if (Actif[i] == 1 && i > 0) { //Les relais en On/Off
+ if (Puissance > MaxTriacPw) { RetardF[i] = 100; } //OFF
+ if (Puissance < SeuilPw) { RetardF[i] = 0; } //On
+ } else { // le Triac ou les relais en sinus
+ RetardF[i] = RetardF[i] + 0.0001; //On ferme très légèrement si pas de message reçu. Sécurité
+ RetardF[i] = RetardF[i] + (Puissance - SeuilPw) * GainBoucle / 10000; // Gain de boucle de l'asservissement
+ if (RetardF[i] < 100 - MaxTriacPw) { RetardF[i] = 100 - MaxTriacPw; }
+ if (ITmode < 0 && i == 0) RetardF[i] = 100; //Triac pas possible sur synchro interne
+ }
+ if (RetardF[i] < 0) { RetardF[i] = 0; }
+ if (RetardF[i] > 100) { RetardF[i] = 100; }
+ }
+ } else {
+ RetardF[i] = 100;
+ }
+ Retard[i] = int(RetardF[i]); //Valeure entiere pour piloter le Triac et les relais
+ if (Retard[i] == 100) { // Force en cas d'arret des IT
+ LesActions[i].Arreter();
+ PulseOn[i] = 0; //Stop Triac ou relais
+ } else {
+
+ switch (Actif[i]) { //valeur en RAM du Mode de regulation
+ case 1: //Decoupe Sinus pour Triac ou On/Off pour relais
+ if (i > 0) LesActions[i].RelaisOn();
+ break;
+ case 2: // Multi Sinus
+ PulseOn[i] = tabPulseSinusOn[100 - Retard[i]];
+ PulseTotal[i] = tabPulseSinusTotal[100 - Retard[i]];
+ break;
+ case 3: // Train de Sinus
+ PulseOn[i] = 100 - Retard[i];
+ PulseTotal[i] = 99; //Nombre impair pour Ă©viter courant continu
+ break;
+ }
+ }
+ }
+ LissageLong = lissage;
+}
+
+void InitGpioActions() {
+ for (int i = 1; i < NbActions; i++) {
+ LesActions[i].InitGpio();
+ Gpio[i] = LesActions[i].Gpio;
+ OutOn[i] = LesActions[i].OutOn;
+ OutOff[i] = LesActions[i].OutOff;
+ }
+}
+// ***********************************
+// * Calage ZĂ©ro Energie quotidienne * -
+// ***********************************
+
+void EnergieQuotidienne() {
+ if (DATEvalid && Source != "Ext") {
+ if (Energie_M_Soutiree < EAS_M_J0 || EAS_M_J0 == 0) {
+ EAS_M_J0 = Energie_M_Soutiree;
+ }
+ EnergieJour_M_Soutiree = Energie_M_Soutiree - EAS_M_J0;
+ if (Energie_M_Injectee < EAI_M_J0 || EAI_M_J0 == 0) {
+ EAI_M_J0 = Energie_M_Injectee;
+ }
+ EnergieJour_M_Injectee = Energie_M_Injectee - EAI_M_J0;
+ if (Energie_T_Soutiree < EAS_T_J0 || EAS_T_J0 == 0) {
+ EAS_T_J0 = Energie_T_Soutiree;
+ }
+ EnergieJour_T_Soutiree = Energie_T_Soutiree - EAS_T_J0;
+ if (Energie_T_Injectee < EAI_T_J0 || EAI_T_J0 == 0) {
+ EAI_T_J0 = Energie_T_Injectee;
+ }
+ EnergieJour_T_Injectee = Energie_T_Injectee - EAI_T_J0;
+ }
+}
+
+// **************
+// * Heure DATE * -
+// **************
+void time_sync_notification(struct timeval *tv) {
+ Serial.println("Notification de l'heure ( time synchronization event ) ");
+ DATEvalid = true;
+ Serial.print("Sync time in ms : ");
+ Serial.println(sntp_get_sync_interval());
+ JourHeureChange();
+ StockMessage("RĂ©ception de l'heure");
+}
+
+
+//****************
+//* Gestion LEDs *
+//****************
+void Gestion_LEDs() {
+ int retard_min = 100;
+ int retardI;
+ cptLEDyellow++;
+ if (WiFi.status() != WL_CONNECTED) { // Attente connexion au Wifi
+ if (WiFi.getMode() == WIFI_STA) { // en Station mode
+ cptLEDyellow = (cptLEDyellow + 6) % 10;
+ cptLEDgreen = cptLEDyellow;
+ } else { //AP Mode
+ cptLEDyellow = cptLEDyellow % 10;
+ cptLEDgreen = (cptLEDyellow + 5) % 10;
+ }
+ } else {
+ for (int i = 0; i < NbActions; i++) {
+ retardI = Retard[i];
+ retard_min = min(retard_min, retardI);
+ }
+ if (retard_min < 100) {
+ cptLEDgreen = int((cptLEDgreen + 1 + 8 / (1 + retard_min / 10))) % 10;
+ } else {
+ cptLEDgreen = 10;
+ }
+ }
+ if (cptLEDyellow > 5) {
+ digitalWrite(LedYellow, LOW);
+ } else {
+ digitalWrite(LedYellow, HIGH);
+ }
+ if (cptLEDgreen > 5) {
+ digitalWrite(LedGreen, LOW);
+ } else {
+ digitalWrite(LedGreen, HIGH);
+ }
+}
+//*************
+//* Test Pmax *
+//*************
+float PfloatMax(float Pin) {
+ float P = max(-PmaxReseau, Pin);
+ P = min(PmaxReseau, P);
+ return P;
+}
+int PintMax(int Pin) {
+ int M = int(PmaxReseau);
+ int P = max(-M, Pin);
+ P = min(M, P);
+ return P;
+}
+//****************************************************
+//* Comportement etrange depuis V3.0.1 de l'ESP32 *
+// le watchdog se reset si le wifi plante. A creuser *
+// Work around en attendant *
+//****************************************************
+void ComAbuge() {
+ ComBug++;
+ if (ComBug < 200) {
+ esp_task_wdt_reset();
+ }
+}
+void ComOK() {
+ ComBug = 0;
+ esp_task_wdt_reset(); // Reset du Watchdog
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino
new file mode 100644
index 0000000..6f1812d
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino
@@ -0,0 +1,300 @@
+
+void Setup_Enphase() {
+
+ //Obtention Session ID
+ //********************
+ const char* server1Enphase = "enlighten.enphaseenergy.com";
+ String Host = String(server1Enphase);
+ String adrEnphase = "https://" + Host + "/login/login.json";
+ String requestBody = "user[email]=" + EnphaseUser + "&user[password]=" + urlEncode( EnphasePwd);
+
+ if (EnphaseUser != "" && EnphasePwd != "") {
+ Serial.println("Essai connexion Enlighten server 1 pour obtention session_id!");
+ clientSecu.setInsecure(); //skip verification
+ if (!clientSecu.connect(server1Enphase, 443))
+ StockMessage("Connection failed to Enlighten server :" + Host);
+ else {
+ Serial.println("Connected to Enlighten server:" + Host);
+ clientSecu.println("POST " + adrEnphase + "?" + requestBody + " HTTP/1.0");
+ clientSecu.println("Host: " + Host);
+ clientSecu.println("Connection: close");
+ clientSecu.println();
+ String line = "";
+ while (clientSecu.connected()) {
+ line = clientSecu.readStringUntil('\n');
+ if (line == "\r") {
+ Serial.println("headers 1 Enlighten received");
+ JsonToken = "";
+ }
+
+ JsonToken += line;
+ }
+ // if there are incoming bytes available
+ // from the server, read them and print them:
+ while (clientSecu.available()) {
+ char c = clientSecu.read();
+ Serial.write(c);
+ }
+ clientSecu.stop();
+ }
+ Session_id = StringJson("session_id", JsonToken);
+ Serial.println("session_id :" + Session_id);
+ } else {
+ Serial.println("Connexion vers Envoy-S en firmware version 5");
+ }
+ //Obtention Token
+ //********************
+ if (Session_id != "" && EnphaseSerial != "" && EnphaseUser != "") {
+ const char* server2Enphase = "entrez.enphaseenergy.com";
+ Host = String(server2Enphase);
+ adrEnphase = "https://" + Host + "/tokens";
+ requestBody = "{\"session_id\":\"" + Session_id + "\", \"serial_num\":" + EnphaseSerial + ", \"username\":\"" + EnphaseUser + "\"}";
+ Serial.println("Essai connexion Enlighten server 2 pour obtention token!");
+ clientSecu.setInsecure(); //skip verification
+ if (!clientSecu.connect(server2Enphase, 443))
+ StockMessage("Connection failed to :" + Host);
+ else {
+ Serial.println("Connected to :" + Host);
+ clientSecu.println("POST " + adrEnphase + " HTTP/1.0");
+ clientSecu.println("Host: " + Host);
+ clientSecu.println("Content-Type: application/json");
+ clientSecu.println("content-length:" + String(requestBody.length()));
+ clientSecu.println("Connection: close");
+ clientSecu.println();
+ clientSecu.println(requestBody);
+ clientSecu.println();
+ Serial.println("Attente user est connecté");
+ String line = "";
+ JsonToken = "";
+ while (clientSecu.connected()) {
+ line = clientSecu.readStringUntil('\n');
+ if (line == "\r") {
+ Serial.println("headers 2 enlighten received");
+ JsonToken = "";
+ }
+
+ JsonToken += line;
+ }
+ // if there are incoming bytes available
+ // from the server, read them and print them:
+ while (clientSecu.available()) {
+ char c = clientSecu.read();
+ Serial.write(c);
+ }
+ clientSecu.stop();
+ JsonToken.trim();
+ Serial.println("Token :" + JsonToken);
+ if (JsonToken.length() > 50) {
+ TokenEnphase = JsonToken;
+ previousTimeRMSMin = 1000;
+ previousTimeRMSMax = 1;
+ previousTimeRMSMoy = 1;
+ previousTimeRMS = millis();
+ LastRMS_Millis = millis();
+ PeriodeProgMillis = 1000;
+ }
+ }
+ }
+}
+
+void LectureEnphase() { //Lecture des consommations
+ int Num_portIQ = 443;
+ String JsonEnPhase = "";
+ byte arr[4];
+ arr[0] = RMSextIP & 0xFF; // 0x78
+ arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56
+ arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34
+ arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+
+ if (TokenEnphase.length() > 50 && EnphaseUser != "") { //Connexion por firmware V7
+ if (millis() > 2592000000) { //Tout les 30 jours on recherche un nouveau Token
+ Setup_Enphase();
+ }
+
+ clientSecu.setInsecure(); //skip verification
+ if (!clientSecu.connect(host.c_str(), Num_portIQ)) {
+ StockMessage("Connection failed to Envoy-S server! : " + host);
+ } else {
+ //Serial.println("Connected to Envoy-S server!");
+ clientSecu.println("GET https://" + host + "/ivp/meters/reports/consumption HTTP/1.0");
+ clientSecu.println("Host: " + host);
+ clientSecu.println("Accept: application/json");
+ clientSecu.println("Authorization: Bearer " + TokenEnphase);
+ clientSecu.println("Connection: close");
+ clientSecu.println();
+
+ String line = "";
+ while (clientSecu.connected()) {
+ line = clientSecu.readStringUntil('\n');
+ if (line == "\r") {
+ //Serial.println("headers received");
+ JsonEnPhase = "";
+ }
+ JsonEnPhase += line;
+ }
+ // if there are incoming bytes available
+ // from the server, read them and print them:
+ while (clientSecu.available()) {
+ char c = clientSecu.read();
+ Serial.write(c);
+ }
+
+ clientSecu.stop();
+ }
+ } else { // Conexion Envoy V5
+ // Use WiFiClient class to create TCP connections http
+ WiFiClient clientFirmV5;
+ if (!clientFirmV5.connect(host.c_str(), 80)) {
+ StockMessage("connection to client clientFirmV5 failed (call to Envoy-S)");
+ delay(200);
+ ComAbuge();
+ return;
+ }
+ String url = "/ivp/meters/reports/consumption";
+ clientFirmV5.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");;
+ unsigned long timeout = millis();
+ while (clientFirmV5.available() == 0) {
+ if (millis() - timeout > 5000) {
+ Serial.println(">>> client clientFirmV5 Timeout !");
+ clientFirmV5.stop();
+ return;
+ }
+ }
+ timeout = millis();
+ String line;
+ // Lecture des données brutes distantes
+ while (clientFirmV5.available() && (millis() - timeout < 5000)) {
+ line = clientFirmV5.readStringUntil('\n');
+ if (line == "\r") {
+ //Serial.println("headers received");
+ JsonEnPhase = "";
+ }
+ JsonEnPhase += line;
+ }
+ }
+
+ // On utilise pas la librairie ArduinoJson.h, pour décoder message Json, qui crache sur de grosses données
+ String TotConso = PrefiltreJson("total-consumption", "cumulative", JsonEnPhase);
+ PactConso_M = int(ValJson("actPower", TotConso));
+ String NetConso = PrefiltreJson("net-consumption", "cumulative", JsonEnPhase);
+ float PactReseau = ValJson("actPower", NetConso);
+ PactReseau = PfloatMax(PactReseau);
+ if (PactReseau < 0) {
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = int(-PactReseau);
+ } else {
+ PuissanceI_M_inst = 0;
+ PuissanceS_M_inst = int(PactReseau);
+ }
+ float PvaReseau = ValJson("apprntPwr", NetConso);
+ PvaReseau = PfloatMax(PvaReseau);
+ if (PvaReseau < 0) {
+ PVAS_M_inst = 0;
+ PVAI_M_inst = int(-PvaReseau);
+ } else {
+ PVAI_M_inst = 0;
+ PVAS_M_inst = int(PvaReseau);
+ }
+ Pva_valide=true;
+ filtre_puissance();
+ float PowerFactor = 0;
+ if ((PVA_M_moy ) != 0) {
+ PowerFactor = floor(100 * abs(Puissance_M_moy ) / PVA_M_moy ) / 100;
+ PowerFactor = min(PowerFactor, float(1));
+ }
+ PowerFactor_M = PowerFactor;
+ long whDlvdCum = LongJson("whDlvdCum", NetConso);
+ long DeltaWh = 0;
+ if (whDlvdCum != 0) { // bonne donnée
+ if (LastwhDlvdCum == 0) {
+ LastwhDlvdCum = whDlvdCum;
+ }
+ DeltaWh = whDlvdCum - LastwhDlvdCum;
+ LastwhDlvdCum = whDlvdCum;
+ if (DeltaWh < 0) {
+ Energie_M_Injectee = Energie_M_Injectee - DeltaWh;
+ } else {
+ Energie_M_Soutiree = Energie_M_Soutiree + DeltaWh;
+ }
+ }
+ Tension_M = ValJson("rmsVoltage", NetConso);
+ Intensite_M = ValJson("rmsCurrent", NetConso);
+ PactProd = PactConso_M - int(PactReseau);
+ EnergieActiveValide = true;
+ if (PactReseau != 0 || PvaReseau != 0) {
+ ComOK(); //Reset du Watchdog à chaque trame reçue de la passerelle Envoy-S metered
+ }
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+}
+
+String PrefiltreJson(String F1, String F2, String Json) {
+ int p = Json.indexOf(F1);
+ Json = Json.substring(p);
+ p = Json.indexOf(F2);
+ Json = Json.substring(p);
+ return Json;
+}
+
+float ValJson(String nom, String Json) {
+ int p = Json.indexOf(nom);
+ Json = Json.substring(p);
+ p = Json.indexOf(":");
+ Json = Json.substring(p + 1);
+ int q = Json.indexOf(",");
+ p = Json.indexOf("}");
+ p = min(p, q);
+ float val = 0;
+ if (p > 0) {
+ Json = Json.substring(0, p);
+ val = Json.toFloat();
+ }
+ return val;
+}
+long LongJson(String nom, String Json) { // Pour éviter des problèmes d'overflow
+ int p = Json.indexOf(nom);
+ Json = Json.substring(p);
+ p = Json.indexOf(":");
+ Json = Json.substring(p + 1);
+ int q = Json.indexOf(".");
+ p = Json.indexOf("}");
+ p = min(p, q);
+ long val = 0;
+ if (p > 0) {
+ Json = Json.substring(0, p);
+ val = Json.toInt();
+ }
+ return val;
+}
+
+long myLongJson(String nom, String Json) { // Alternative a LongJson au dessus pour extraire chez EDF nb jour Tempo https://particulier.edf.fr/services/rest/referentiel/getNbTempoDays?TypeAlerte=TEMPO
+ int p = Json.indexOf(nom);
+ Json = Json.substring(p);
+ p = Json.indexOf(":");
+ Json = Json.substring(p + 1);
+ int q = Json.indexOf(",");//<==== Recherche d'une virgule et non d'un point
+ if (q == -1) q = 999; // /<==== Ajout de ces 2 lignes pour que la ligne p = min(p, q); ci dessous donne le bon résultat
+ p = Json.indexOf("}");
+ p = min(p, q);
+ long val = 0;
+ if (p > 0) {
+ Json = Json.substring(0, p);
+ val = Json.toInt();
+ }
+ return val;
+}
+
+
+String StringJson(String nom, String Json) {
+ int p = Json.indexOf(nom);
+ Json = Json.substring(p);
+ p = Json.indexOf(":");
+ Json = Json.substring(p + 1);
+ p = Json.indexOf("\"");
+ Json = Json.substring(p + 1);
+ p = Json.indexOf("\"");
+ Json = Json.substring(0, p);
+ return Json;
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino
new file mode 100644
index 0000000..a3f65f3
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino
@@ -0,0 +1,143 @@
+// ***************************************************************
+// * Client d'un autre ESP32 en charge de mesurer les puissances *
+// ***************************************************************
+void CallESP32_Externe() {
+ String S = "";
+ String RMSExtDataB = "";
+ String Gr[4];
+ String data_[22];
+
+
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientESP_RMS;
+ byte arr[4];
+ arr[0] = RMSextIP & 0xFF; // 0x78
+ arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56
+ arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34
+ arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12
+
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ if (!clientESP_RMS.connect(host.c_str(), 80)) {
+ ComAbuge();
+ StockMessage("connection to ESP_RMS : " + host +" failed");
+ delay(200);
+ return;
+ }
+ String url = "/ajax_data";
+ clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientESP_RMS.available() == 0) {
+ if (millis() - timeout > 5000) {
+
+ StockMessage("client ESP_RMS Timeout !" + host);
+
+ clientESP_RMS.stop();
+ return;
+ }
+ }
+ timeout = millis();
+ // Lecture des données brutes distantes
+ while (clientESP_RMS.available() && (millis() - timeout < 5000)) {
+ RMSExtDataB += clientESP_RMS.readStringUntil('\r');
+ }
+ if (RMSExtDataB.length() > 300) {
+ RMSExtDataB = "";
+ }
+ if (RMSExtDataB.indexOf("Deb") >= 0 && RMSExtDataB.indexOf("Fin") > 0) { //Trame complète reçue
+ RMSExtDataB = RMSExtDataB.substring(RMSExtDataB.indexOf("Deb") + 4);
+ RMSExtDataB = RMSExtDataB.substring(0, RMSExtDataB.indexOf("Fin") + 3);
+ String Sval = "";
+ int idx = 0;
+ while (RMSExtDataB.indexOf(GS) > 0) {
+ Sval = RMSExtDataB.substring(0, RMSExtDataB.indexOf(GS));
+ RMSExtDataB = RMSExtDataB.substring(RMSExtDataB.indexOf(GS) + 1);
+ Gr[idx] = Sval;
+ idx++;
+ }
+ Gr[idx] = RMSExtDataB;
+ idx = 0;
+ for (int i = 0; i < 3; i++) {
+ while (Gr[i].indexOf(RS) >= 0) {
+ Sval = Gr[i].substring(0, Gr[i].indexOf(RS));
+ Gr[i] = Gr[i].substring(Gr[i].indexOf(RS) + 1);
+ data_[idx] = Sval;
+ idx++;
+ }
+ data_[idx] = Gr[i];
+ idx++;
+ }
+ for (int i = 0; i <= idx; i++) {
+ switch (i) {
+
+ case 1:
+ Source_data = data_[i];
+ break;
+ case 2:
+ if (TempoEDFon == 0) LTARF = data_[i];
+ break;
+ case 3:
+ if (TempoEDFon == 0) STGE = data_[i];
+ break;
+ case 4:
+ //Temperature non utilisé
+ break;
+ case 5:
+ Pva_valide=data_[i].toInt();
+ break;
+ case 6:
+ PuissanceS_M = PintMax(data_[i].toInt());
+ break;
+ case 7:
+ PuissanceI_M = PintMax(data_[i].toInt());
+ break;
+ case 8:
+ PVAS_M = PintMax(data_[i].toInt());
+ break;
+ case 9:
+ PVAI_M = PintMax(data_[i].toInt());
+ break;
+ case 10:
+ EnergieJour_M_Soutiree = data_[i].toInt();
+ break;
+ case 11:
+ EnergieJour_M_Injectee = data_[i].toInt();
+ break;
+ case 12:
+ Energie_M_Soutiree = data_[i].toInt();
+ break;
+ case 13:
+ Energie_M_Injectee = data_[i].toInt();
+ ComOK(); //Reset du Watchdog à chaque trame du RMS reçue
+ cptLEDyellow = 4;
+ EnergieActiveValide=true;
+ break;
+ case 14: //CAS UxIx2 avec une deuxieme sonde
+ PuissanceS_T = data_[i].toInt();
+ break;
+ case 15:
+ PuissanceI_T = data_[i].toInt();
+ break;
+ case 16:
+ PVAS_T = data_[i].toInt();
+ break;
+ case 17:
+ PVAI_T = data_[i].toInt();
+ break;
+ case 18:
+ EnergieJour_T_Soutiree = data_[i].toInt();
+ break;
+ case 19:
+ EnergieJour_T_Injectee = data_[i].toInt();
+ break;
+ case 20:
+ Energie_T_Soutiree = data_[i].toInt();
+ break;
+ case 21:
+ Energie_T_Injectee = data_[i].toInt();
+ break;
+ }
+
+ }
+ RMSExtDataB = "";
+ }
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino
new file mode 100644
index 0000000..fa01904
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino
@@ -0,0 +1,181 @@
+// ****************************
+// * Source de Mesures LINKY *
+// ****************************
+
+float deltaWS = 0;
+float deltaWI = 0;
+int boucle_appel_Linky = 0;
+void Setup_Linky() {
+ delay(20);
+ MySerial.setRxBufferSize(SER_BUF_SIZE);
+ MySerial.begin(9600, SERIAL_7E1, RXD2, TXD2); // 7-bit Even parity 1 stop bit pour le Linky
+ delay(100);
+}
+
+void LectureLinky() { //Lecture port série du LINKY .
+ int V = 0;
+ long OldWh = 0;
+ float deltaWh = 0;
+ float Pmax = 0;
+ float Pmin = 0;
+ unsigned long Tm = 0;
+ float deltaT = 0;
+ boucle_appel_Linky++;
+ if (boucle_appel_Linky > 4000) {
+ boucle_appel_Linky = 0;
+ MySerial.flush();
+ MySerial.write("Ok");
+ StockMessage("Attente Linky 4000 boucles = 8s");
+ }
+ while (MySerial.available() > 0) {
+ boucle_appel_Linky = 0;
+ V = MySerial.read();
+ DataRawLinky[IdxDataRawLinky] = char(V);
+ IdxDataRawLinky = (IdxDataRawLinky + 1) % 10000;
+ switch (V) {
+ case 2: //STX (Start Text)
+ break;
+ case 3: //ETX (End Text)
+ previousETX = millis();
+ cptLEDyellow = 4;
+ LFon = false;
+ break;
+ case 10: // Line Feed. Debut Groupe
+ LFon = true;
+ IdxBufDecodLinky = IdxDataRawLinky;
+ break;
+ case 13: // Line Feed. Debut Groupe
+ if (LFon) { //Debut groupe OK
+ LFon = false;
+ int nb_tab = 0;
+ String code = "";
+ String val = "";
+ int checksum = 0;
+ int checkLinky = -1;
+
+ while (IdxBufDecodLinky != IdxDataRawLinky) {
+ if (DataRawLinky[IdxBufDecodLinky] == char(9)) { //Tabulation
+ nb_tab++;
+ } else {
+ if (nb_tab == 0) {
+ code += DataRawLinky[IdxBufDecodLinky];
+ }
+ if (nb_tab == 1) {
+ val += DataRawLinky[IdxBufDecodLinky];
+ }
+ if (nb_tab <= 1) {
+ checksum += (int)DataRawLinky[IdxBufDecodLinky];
+ }
+ }
+ IdxBufDecodLinky = (IdxBufDecodLinky + 1) % 10000;
+ if (checkLinky == -1 && nb_tab == 2) {
+ checkLinky = (int)DataRawLinky[IdxBufDecodLinky];
+ checksum += 18; //2 tabulations
+ checksum = checksum & 63; //0x3F
+ checksum = checksum + 32; //0x20
+ }
+ }
+ if (code.indexOf("EAST") == 0 || code.indexOf("EAIT") == 0 || code == "SINSTS" || code.indexOf("SINSTI") == 0) {
+ if (checksum != checkLinky) {
+ StockMessage("Erreur checksum code : " + code + " " + String(checksum) + "," + String(checkLinky));
+ } else {
+ if (code.indexOf("EAST") == 0) {
+
+ OldWh = Energie_M_Soutiree;
+ if (OldWh == 0) { OldWh = val.toInt(); }
+ Energie_M_Soutiree = val.toInt();
+ Tm = millis();
+ deltaT = float(Tm - TlastEASTvalide);
+ deltaT = deltaT / float(3600000);
+ if (Energie_M_Soutiree == OldWh) { //Pas de resultat en Wh
+ Pmax = 1.3 / deltaT;
+ moyPWS = min(moyPWS, Pmax);
+ } else {
+ TlastEASTvalide = Tm;
+ deltaWh = float(Energie_M_Soutiree - OldWh);
+ deltaWS = deltaWh / deltaT;
+ Pmin = (deltaWh - 1) / deltaT;
+ moyPWS = max(moyPWS, Pmin); //saut à la montée en puissance
+ }
+ moyPWS = 0.05 * deltaWS + 0.95 * moyPWS;
+ EASTvalid = true;
+ if (!EAITvalid && Tm > 8000) { //Cas des CACSI ou EAIT n'est jamais positionné
+ EAITvalid = true;
+ }
+ }
+ if (code.indexOf("EAIT") == 0) {
+ OldWh = Energie_M_Injectee;
+ if (OldWh == 0) { OldWh = val.toInt(); }
+ Energie_M_Injectee = val.toInt();
+ Tm = millis();
+ deltaT = float(Tm - TlastEAITvalide);
+ deltaT = deltaT / float(3600000);
+ if (Energie_M_Injectee == OldWh) { //Pas de resultat en Wh
+ Pmax = 1.3 / deltaT;
+ moyPWI = min(moyPWI, Pmax);
+ } else {
+ TlastEAITvalide = Tm;
+ deltaWh = float(Energie_M_Injectee - OldWh);
+ deltaWI = deltaWh / deltaT;
+ Pmin = (deltaWh - 1) / deltaT;
+ moyPWI = max(moyPWI, Pmin); //saut à la montée en puissance
+ }
+ moyPWI = 0.05 * deltaWI + 0.95 * moyPWI;
+ EAITvalid = true;
+ }
+ if (EASTvalid && EAITvalid) {
+ EnergieActiveValide = true;
+ }
+ if (code == "SINSTS") { //Puissance apparente soutirée. Egalité pour ne pas confondre avec SINSTS1 (triphasé)
+ PVAS_M = PintMax(val.toInt());
+ moyPVAS = 0.05 * float(PVAS_M) + 0.95 * moyPVAS;
+ moyPWS = min(moyPWS, moyPVAS);
+ if (moyPVAS > 0) {
+ COSphiS = moyPWS / moyPVAS;
+ COSphiS = min(float(1.0), COSphiS);
+ PowerFactor_M = COSphiS;
+ }
+ PuissanceS_M = PintMax(int(COSphiS * float(PVAS_M)));
+ Pva_valide=true;
+ }
+ if (code.indexOf("SINSTI") == 0) { //Puissance apparente injectée
+ PVAI_M = PintMax(val.toInt());
+ moyPVAI = 0.05 * float(PVAI_M) + 0.95 * moyPVAI;
+ moyPWI = min(moyPWI, moyPVAI);
+ if (moyPVAI > 0) {
+ COSphiI = moyPWI / moyPVAI;
+ COSphiI = min(float(1.0), COSphiI);
+ PowerFactor_M = COSphiI;
+ }
+ PuissanceI_M = PintMax(int(COSphiI * float(PVAI_M)));
+ Pva_valide=true;
+ }
+ }
+ }
+ if (code.indexOf("DATE") == 0) {
+ ComOK(); //Reset du Watchdog à chaque trame du Linky reçue
+ }
+ if (code.indexOf("URMS1") == 0) {
+ Tension_M = val.toFloat(); //phase 1 uniquement
+ }
+ if (code.indexOf("IRMS1") == 0) {
+ Intensite_M = val.toFloat(); //Phase 1 uniquement
+ }
+ if (TempoEDFon == 0) { // On prend tarif sur Linky
+ if (code.indexOf("LTARF") == 0) {
+ LTARF = val; //Option Tarifaire
+ LTARF.trim();
+ }
+ if (code.indexOf("STGE") == 0) {
+ STGE = val; //Status
+ STGE.trim();
+ STGE = STGE.substring(1, 2); //Tempo lendemain et jour sur 1 octet
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino
new file mode 100644
index 0000000..1aca8de
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino
@@ -0,0 +1,54 @@
+
+// ******************************************************
+// * Informations de puissance reçue via un Broker MQTT *
+// ******************************************************
+void UpdatePmqtt() {
+ float Pw = PfloatMax(PwMQTT);
+ float Pf = 1;
+ if (P_MQTT_Brute.indexOf("Pf") > 0) {
+ Pf = abs(PfMQTT);
+ }
+ if (P_MQTT_Brute.indexOf("Pva") > 0) {
+ if (PvaMQTT != 0) {
+ Pf = abs(Pw / PfloatMax(PvaMQTT));
+ }
+ }
+ if (P_MQTT_Brute.indexOf("Pva") > 0 || P_MQTT_Brute.indexOf("Pf") > 0) {
+ Pva_valide = true;
+ } else {
+ Pva_valide = false;
+ }
+ if (Pf > 1) Pf = 1;
+ if (Pw >= 0) {
+ PuissanceS_M_inst = Pw;
+ PuissanceI_M_inst = 0;
+ if (Pf > 0.01) {
+ PVAS_M_inst = PfloatMax(Pw / Pf);
+ } else {
+ PVAS_M_inst = 0;
+ }
+ PVAI_M_inst = 0;
+ EASfloat += Pw / 6000; // Watt Hour,Every 600ms. Soutirée
+ Energie_M_Soutiree = int(EASfloat); // Watt Hour,Every 40ms. Soutirée
+ } else {
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = -Pw;
+ if (Pf > 0.01) {
+ PVAI_M_inst = PfloatMax(-Pw / Pf);
+ } else {
+ PVAI_M_inst = 0;
+ }
+ PVAS_M_inst = 0;
+ EAIfloat += -Pw / 6000;
+ Energie_M_Injectee = int(EAIfloat);
+ }
+
+ filtre_puissance();
+
+ if (P_MQTT_Brute.indexOf("Pw") > 0) EnergieActiveValide = true;
+ if (millis() - LastPwMQTTMillis < 30000) ComOK(); //Reset du Watchdog si trame MQTT reçue avec au minimum Pw récente
+
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino
new file mode 100644
index 0000000..2444e0b
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino
@@ -0,0 +1,177 @@
+// ****************************************************
+// * Client d'un Shelly Em sur voie 0 ou 1 ou triphasé*
+// ****************************************************
+void LectureShellyEm() {
+ String S = "";
+ String Shelly_Data = "";
+ float Pw = 0;
+ float voltage = 0;
+ float pf = 0;
+
+
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientESP_RMS;
+ byte arr[4];
+ arr[0] = RMSextIP & 0xFF; // 0x78
+ arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56
+ arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34
+ arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12
+
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ if (!clientESP_RMS.connect(host.c_str(), 80)) {
+ StockMessage("connection to Shelly Em failed : " + host);
+ delay(200);
+ ComAbuge();
+ return;
+ }
+ int voie = EnphaseSerial.toInt();
+ int Voie = voie % 2;
+
+ if (ShEm_comptage_appels == 1) {
+ Voie = (Voie + 1) % 2;
+ }
+ String url = "/emeter/" + String(Voie);
+ if (voie == 3) url = "/status"; //Triphasé
+ ShEm_comptage_appels = (ShEm_comptage_appels + 1) % 5; // 1 appel sur 6 vers la deuxième voie qui ne sert pas au routeur
+ clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientESP_RMS.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage("client Shelly Em Timeout ! : " + host);
+ clientESP_RMS.stop();
+ return;
+ }
+ }
+ timeout = millis();
+ // Lecture des données brutes distantes
+ while (clientESP_RMS.available() && (millis() - timeout < 5000)) {
+ Shelly_Data += clientESP_RMS.readStringUntil('\r');
+ }
+ int p = Shelly_Data.indexOf("{");
+ Shelly_Data = Shelly_Data.substring(p);
+ if (voie == 3) { //Triphasé
+ ShEm_dataBrute = "Triphasé
" + Shelly_Data;
+ p = Shelly_Data.indexOf("emeters");
+ Shelly_Data = Shelly_Data.substring(p + 10);
+ Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 1
+ pf = ValJson("pf", Shelly_Data);
+ pf = abs(pf);
+ float total_Pw = Pw;
+ float total_Pva = 0;
+ if (pf > 0) {
+ total_Pva = abs(Pw) / pf;
+ }
+ float total_E_soutire = ValJson("total\"", Shelly_Data);
+ float total_E_injecte = ValJson("total_returned", Shelly_Data);
+ p = Shelly_Data.indexOf("}");
+ Shelly_Data = Shelly_Data.substring(p + 1);
+ Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 2
+ pf = ValJson("pf", Shelly_Data);
+ pf = abs(pf);
+ total_Pw += Pw;
+ if (pf > 0) {
+ total_Pva += abs(Pw) / pf;
+ }
+ total_E_soutire += ValJson("total\"", Shelly_Data);
+ total_E_injecte += ValJson("total_returned", Shelly_Data);
+ p = Shelly_Data.indexOf("}");
+ Shelly_Data = Shelly_Data.substring(p + 1);
+ Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 3
+ pf = ValJson("pf", Shelly_Data);
+ pf = abs(pf);
+ total_Pw += Pw;
+ if (pf > 0) {
+ total_Pva += abs(Pw) / pf;
+ }
+ total_E_soutire += ValJson("total\"", Shelly_Data);
+ total_E_injecte += ValJson("total_returned", Shelly_Data);
+ Energie_M_Soutiree = int(total_E_soutire);
+ Energie_M_Injectee = int(total_E_injecte);
+ if (total_Pw == 0) {
+ total_Pva = 0;
+ }
+ if (total_Pw > 0) {
+ PuissanceS_M_inst = total_Pw;
+ PuissanceI_M_inst = 0;
+ PVAS_M_inst = total_Pva;
+ PVAI_M_inst = 0;
+ } else {
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = -total_Pw;
+ PVAI_M_inst = total_Pva;
+ PVAS_M_inst = 0;
+ }
+ } else { //Monophasé
+ ShEm_dataBrute = "Voie : " + String(voie) + "
" + Shelly_Data;
+ Shelly_Data = Shelly_Data + ",";
+ if (Shelly_Data.indexOf("true") > 0) { // Donnée valide
+ Pw = PfloatMax(ValJson("power", Shelly_Data));
+ voltage = ValJson("voltage", Shelly_Data);
+ pf = ValJson("pf", Shelly_Data);
+ pf = abs(pf);
+ if (pf > 1) pf = 1;
+ if (Voie == voie) { //voie du routeur
+ if (Pw >= 0) {
+ PuissanceS_M_inst = Pw;
+ PuissanceI_M_inst = 0;
+ if (pf > 0.01) {
+ PVAS_M_inst = PfloatMax(Pw / pf);
+ } else {
+ PVAS_M_inst = 0;
+ }
+ PVAI_M_inst = 0;
+ } else {
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = -Pw;
+ if (pf > 0.01) {
+ PVAI_M_inst = PfloatMax(-Pw / pf);
+ } else {
+ PVAI_M_inst = 0;
+ }
+ PVAS_M_inst = 0;
+ }
+ Energie_M_Soutiree = int(ValJson("total\"", Shelly_Data));
+ Energie_M_Injectee = int(ValJson("total_returned", Shelly_Data));
+ PowerFactor_M = pf;
+ Tension_M = voltage;
+ Pva_valide=true;
+ } else { // voie secondaire
+ if (LissageLong) {
+ PwMoy2 = 0.2 * Pw + 0.8 * PwMoy2; //Lissage car moins de mesure sur voie secondaire
+ pfMoy2 = 0.2 * pf + 0.8 * pfMoy2;
+ Pw = PwMoy2;
+ pf = pfMoy2;
+ }
+ if (Pw >= 0) {
+ PuissanceS_T_inst = Pw;
+ PuissanceI_T_inst = 0;
+ if (pf > 0.01) {
+ PVAS_T_inst = PfloatMax(Pw / pf);
+ } else {
+ PVAS_T_inst = 0;
+ }
+ PVAI_T_inst = 0;
+ } else {
+ PuissanceS_T_inst = 0;
+ PuissanceI_T_inst = -Pw;
+ if (pf > 0.01) {
+ PVAI_T_inst = PfloatMax(-Pw / pf);
+ } else {
+ PVAI_T_inst = 0;
+ }
+ PVAS_T_inst = 0;
+ }
+ Energie_T_Soutiree = int(ValJson("total\"", Shelly_Data));
+ Energie_T_Injectee = int(ValJson("total_returned", Shelly_Data));
+ PowerFactor_T = pf;
+ Tension_T = voltage;
+ }
+ }
+ }
+ filtre_puissance();
+ ComOK(); //Reset du Watchdog à chaque trame du Shelly reçue
+ if (ShEm_comptage_appels > 1) EnergieActiveValide = true;
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino
new file mode 100644
index 0000000..b3ced44
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino
@@ -0,0 +1,75 @@
+// ******************************
+// * Client d'un Smart Gateways *
+// ******************************
+void LectureSmartG() {
+ String S = "";
+ String SmartG_Data = "";
+ String Gr[4];
+ String data_[20];
+
+ Pva_valide=false;
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientESP_RMS;
+ byte arr[4];
+ arr[0] = RMSextIP & 0xFF; // 0x78
+ arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56
+ arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34
+ arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12
+
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ if (!clientESP_RMS.connect(host.c_str(), 82)) { // PORT 82 pour Smlart Gateways
+ StockMessage("connection to SmartGateways failed : " + host);
+ delay(200);
+ ComAbuge();
+ return;
+ }
+ String url = "/smartmeter/api/read";
+ clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientESP_RMS.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage(">>> client SmartGateways Timeout ! : " + host);
+ clientESP_RMS.stop();
+ return;
+ }
+ }
+ timeout = millis();
+ // Lecture des données brutes distantes
+ while (clientESP_RMS.available() && (millis() - timeout < 5000)) {
+ SmartG_Data += clientESP_RMS.readStringUntil('\r');
+ }
+ int p = SmartG_Data.indexOf("{");
+ SmartG_Data = SmartG_Data.substring(p+1);
+ p = SmartG_Data.indexOf("}");
+ SmartG_Data = SmartG_Data.substring(0,p);
+ PuissanceS_M_inst=PfloatMax(ValJsonSG("PowerDelivered_total", SmartG_Data));
+ PuissanceI_M_inst=PfloatMax(ValJsonSG("PowerReturned_total", SmartG_Data));
+ long EnergyDeliveredTariff1=int(1000*ValJsonSG("EnergyDeliveredTariff1", SmartG_Data));
+ long EnergyDeliveredTariff2=int(1000*ValJsonSG("EnergyDeliveredTariff2", SmartG_Data));
+ Energie_M_Soutiree=EnergyDeliveredTariff1+EnergyDeliveredTariff2;
+ long EnergyReturnedTariff1=int(1000*ValJsonSG("EnergyReturnedTariff1", SmartG_Data));
+ long EnergyReturnedTariff2=int(1000*ValJsonSG("EnergyReturnedTariff2", SmartG_Data));
+ Energie_M_Injectee=EnergyReturnedTariff1+EnergyReturnedTariff2;
+ SG_dataBrute=SmartG_Data;
+ filtre_puissance();
+ ComOK(); //Reset du Watchdog à chaque trame du SmartGateways reçue
+ EnergieActiveValide = true;
+
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+}
+
+float ValJsonSG(String nom, String Json) {
+ int p = Json.indexOf(nom);
+ Json = Json.substring(p);
+ p = Json.indexOf(":");
+ Json = Json.substring(p + 2);
+ p = Json.indexOf(",");
+ float val = 0;
+ if (p > 0) {
+ Json = Json.substring(0, p);
+ val = Json.toFloat();
+ }
+ return val;
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino
new file mode 100644
index 0000000..28191b5
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino
@@ -0,0 +1,76 @@
+// ****************************
+// * Source de Mesures U et I *
+// * UXI *
+// ****************************
+
+
+void Setup_UxI() {
+ for (int i = 0; i < 100; i++) { //Reset table measurements
+ voltM[i] = 0;
+ ampM[i] = 0;
+ }
+}
+void LectureUxI() {
+ MeasurePower();
+ ComputePower();
+}
+void MeasurePower() { //Lecture Tension et courants pendant 20ms
+ int iStore;
+ value0 = analogRead(AnalogIn0); //Mean value. Should be at 3.3v/2
+ unsigned long MeasureMillis = millis();
+
+ while (millis() - MeasureMillis < 21) { //Read values in continuous during 20ms. One loop is around 150 micro seconds
+ iStore = (micros() % 20000) / 200; //We have more results that we need during 20ms to fill the tables of 100 samples
+ volt[iStore] = analogRead(AnalogIn1) - value0;
+ amp[iStore] = analogRead(AnalogIn2) - value0;
+ }
+}
+void ComputePower() {
+ float PWcal = 0; //Computation Power in Watt
+ float V;
+ float I;
+ float Uef2 = 0;
+ float Ief2 = 0;
+ for (int i = 0; i < 100; i++) {
+ voltM[i] = (19 * voltM[i] + float(volt[i])) / 20; //Mean value. First Order Filter. Short Integration
+ V = kV * voltM[i];
+ Uef2 += sq(V);
+ ampM[i] = (19 * ampM[i] + float(amp[i])) / 20; //Mean value. First Order Filter
+ I = kI * ampM[i];
+ Ief2 += sq(I);
+ PWcal += V * I;
+ }
+ Uef2 = Uef2 / 100; //square of voltage
+ Tension_M = sqrt(Uef2); //RMS voltage
+ Ief2 = Ief2 / 100; //square of current
+ Intensite_M = sqrt(Ief2); // RMS current
+ PWcal = PfloatMax(PWcal / 100);
+ float PVA =PfloatMax( floor(Tension_M * Intensite_M));
+ float PowerFactor = 0;
+ if (PVA > 0) {
+ PowerFactor = floor(100 * PWcal / PVA) / 100;
+ }
+ PowerFactor_M = PowerFactor;
+ if (PWcal >= 0) {
+ EASfloat += PWcal / 90000; // Watt Hour,Every 40ms. Soutirée
+ Energie_M_Soutiree =int(EASfloat); // Watt Hour,Every 40ms. Soutirée
+ PuissanceS_M_inst = PWcal;
+ PuissanceI_M_inst = 0;
+ PVAS_M_inst = PVA;
+ PVAI_M_inst = 0;
+ } else {
+ EAIfloat += -PWcal / 90000;
+ Energie_M_Injectee =int(EAIfloat);
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = -PWcal;
+ PVAS_M_inst = 0;
+ PVAI_M_inst = PVA;
+ }
+ Pva_valide=true;
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+ filtre_puissance();
+ EnergieActiveValide = true;
+ ComOK();
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino
new file mode 100644
index 0000000..8dc33d1
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino
@@ -0,0 +1,96 @@
+// *******************************
+// * Source de Mesures UI Double *
+// * Capteur JSY-MK-194 *
+// *******************************
+
+void Setup_UxIx2() {
+ MySerial.setRxBufferSize(SER_BUF_SIZE);
+ MySerial.begin(4800, SERIAL_8N1, RXD2, TXD2); //PORT DE CONNEXION AVEC LE CAPTEUR JSY-MK-194
+}
+void LectureUxIx2() { //Ecriture et Lecture port série du JSY-MK-194 .
+
+ int i, j;
+ byte msg_send[] = { 0x01, 0x03, 0x00, 0x48, 0x00, 0x0E, 0x44, 0x18 };
+ // Demande Info sur le Serial port 2 (Modbus RTU)
+ for (i = 0; i < 8; i++) {
+ MySerial.write(msg_send[i]);
+ }
+
+ //Réponse en général à l'appel précédent (seulement 4800bauds)
+ int a = 0;
+ while (MySerial.available()) {
+ ByteArray[a] = MySerial.read();
+ a++;
+ }
+
+
+ if (a == 61) { //Message complet reçu
+ j = 3;
+ for (i = 0; i < 14; i++) { // conversion séries de 4 octets en long
+ LesDatas[i] = 0;
+ LesDatas[i] += ByteArray[j] << 24;
+ j += 1;
+ LesDatas[i] += ByteArray[j] << 16;
+ j += 1;
+ LesDatas[i] += ByteArray[j] << 8;
+ j += 1;
+ LesDatas[i] += ByteArray[j];
+ j += 1;
+ }
+ Sens_1 = ByteArray[27]; // Sens 1
+ Sens_2 = ByteArray[28];
+
+ //Données du Triac
+ Tension_T = LesDatas[0] * .0001;
+ Intensite_T = LesDatas[1] * .0001;
+ float Puiss_1 = PfloatMax(LesDatas[2] * .0001);
+ Energie_T_Soutiree = int(LesDatas[3] * .1);
+ PowerFactor_T = LesDatas[4] * .001;
+ Energie_T_Injectee = int(LesDatas[5] * .1);
+ Frequence = LesDatas[7] * .01;
+ float PVA1 = 0;
+ if (PowerFactor_T > 0) {
+ PVA1 = Puiss_1 / PowerFactor_T;
+ }
+ if (Sens_1 > 0) { //Injection sur TRiac. Ne devrait pas arriver
+ PuissanceI_T_inst = Puiss_1;
+ PuissanceS_T_inst = 0;
+ PVAI_T_inst = PVA1;
+ PVAS_T_inst = 0;
+ } else {
+ PuissanceS_T_inst = Puiss_1;
+ PuissanceI_T_inst = 0;
+ PVAI_T_inst = 0;
+ PVAS_T_inst = PVA1;
+ }
+ // Données générale de la Maison
+ Tension_M = LesDatas[8] * .0001;
+ Intensite_M = LesDatas[9] * .0001;
+ float Puiss_2 = PfloatMax(LesDatas[10] * .0001);
+ Energie_M_Soutiree = int(LesDatas[11] * .1);
+ PowerFactor_M = LesDatas[12] * .001;
+ Energie_M_Injectee = int(LesDatas[13] * .1);
+ float PVA2 = 0;
+ if (PowerFactor_M > 0) {
+ PVA2 = Puiss_2 / PowerFactor_M;
+ }
+ if (Sens_2 > 0) { //Injection en entrée de Maison
+ PuissanceI_M_inst = Puiss_2;
+ PuissanceS_M_inst = 0;
+ PVAI_M_inst = PVA2;
+ PVAS_M_inst = 0;
+ } else {
+ PuissanceS_M_inst = Puiss_2;
+ PuissanceI_M_inst = 0;
+ PVAI_M_inst = 0;
+ PVAS_M_inst = PVA2;
+ }
+ filtre_puissance();
+ EnergieActiveValide = true;
+ Pva_valide = true;
+ ComOK(); //Reset du Watchdog à chaque trame du module JSY-MK-194 reçue
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino
new file mode 100644
index 0000000..a1e08a5
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino
@@ -0,0 +1,125 @@
+// *************************************************
+// * Client lecture JSY-MK-333 * Triphasé *
+// * DĂ©veloppement initial de Pierre F (Mars 2024) *
+// * update PhDV61 Juin 2024 *
+// *************************************************
+
+
+void Setup_JSY333() {
+ MySerial.setRxBufferSize(SER_BUF_SIZE);
+ MySerial.begin(9600, SERIAL_8N1, RXD2, TXD2); //PORT DE CONNEXION AVEC LE CAPTEUR JSY-MK-333
+}
+
+void Lecture_JSY333() {
+ float Tension_M1, Tension_M2, Tension_M3;
+ float Intensite_M1, Intensite_M2, Intensite_M3;
+ float PVA_M_inst1, PVA_M_inst2, PVA_M_inst3;
+ float PW_inst1, PW_inst2, PW_inst3;
+
+ byte Lecture333[200];
+ bool injection;
+ bool sens1, sens2, sens3;
+ long delta_temps = 0;
+
+ int i;
+ byte msg_send[] = { 0x01, 0x03, 0x01, 0x00, 0x00, 0x44, 0x44, 0x05 };
+ for (i = 0; i < 8; i++) {
+ MySerial.write(msg_send[i]);
+ }
+
+ int a = 0;
+ while (MySerial.available()) {
+ Lecture333[a] = MySerial.read();
+ a++;
+ }
+
+ if (a == 141) { //message complet reçu
+ delta_temps = (unsigned long)(millis() - Temps_precedent); // temps écoulé depuis le dernier appel
+ Temps_precedent = millis(); // on conserve la valeur du temps actuel pour le calcul précédent
+
+ Tension_M1 = ((float)(Lecture333[3] * 256 + Lecture333[4])) / 100;
+ Tension_M2 = ((float)(Lecture333[5] * 256 + Lecture333[6])) / 100;
+ Tension_M3 = ((float)(Lecture333[7] * 256 + Lecture333[8])) / 100;
+ Intensite_M1 = ((float)(Lecture333[9] * 256 + Lecture333[10])) / 100;
+ Intensite_M2 = ((float)(Lecture333[11] * 256 + Lecture333[12])) / 100;
+ Intensite_M3 = ((float)(Lecture333[13] * 256 + Lecture333[14])) / 100;
+
+ sens1 = (Lecture333[104]) & 0x01;
+ sens2 = (Lecture333[104] >> 1) & 0x01;
+ sens3 = (Lecture333[104] >> 2) & 0x01;
+
+ if (sens1) { Intensite_M1 *= -1; }
+ if (sens2) { Intensite_M2 *= -1; }
+ if (sens3) { Intensite_M3 *= -1; }
+
+ injection = (Lecture333[104] >> 3) & 0x01; //si sens est true, injection
+
+ // Lecture des Puissances actives de chacune des phases
+ PW_inst1 = (float)(Lecture333[15] * 256.0) + (float)Lecture333[16];
+ PW_inst2 = (float)(Lecture333[17] * 256.0) + (float)Lecture333[18];
+ PW_inst3 = (float)(Lecture333[19] * 256.0) + (float)Lecture333[20];
+
+ //Lecture des puissances apparentes de chacune des phases, qu'on signe comme le Linky
+ PVA_M_inst1 = (float)(Lecture333[35] * 256) + (float)Lecture333[36];
+ if (sens1) { PVA_M_inst1 = -PVA_M_inst1; }
+ PVA_M_inst2 = (float)(Lecture333[37] * 256) + (float)Lecture333[38];
+ if (sens2) { PVA_M_inst2 = -PVA_M_inst2; }
+ PVA_M_inst3 = (float)(Lecture333[39] * 256) + (float)Lecture333[40];
+ if (sens3) { PVA_M_inst3 = -PVA_M_inst3; }
+
+ if (injection) {
+ PuissanceS_M_inst = 0;
+ PuissanceI_M_inst = ((float)((float)(Lecture333[21] * 16777216) + (float)(Lecture333[22] * 65536) + (float)(Lecture333[23] * 256) + (float)Lecture333[24]));
+ PVAS_M_inst = 0;
+ PVAI_M_inst = abs(PVA_M_inst1 + PVA_M_inst2 + PVA_M_inst3); // car la somme des puissances apparentes "signées" est négative puisqu'en "injection" au global
+
+ // PhDV61 : on considère que cette puissance active "globale" a duré "delta_temps", et on l'intègre donc pour obtenir une énergie en Wh
+ Energie_jour_Injectee += ((float)delta_temps / 1000) * (PuissanceI_M_inst / 3600.0);
+
+ } else { // soutirage
+ PuissanceI_M_inst = 0;
+ PuissanceS_M_inst = ((float)((float)(Lecture333[21] * 16777216) + (float)(Lecture333[22] * 65536) + (float)(Lecture333[23] * 256) + (float)Lecture333[24]));
+ PVAI_M_inst = 0;
+ PVAS_M_inst = PVA_M_inst1 + PVA_M_inst2 + PVA_M_inst3;
+
+ // PhDV61 : on considère que cette puissance active "globale" a duré "delta_temps", et on l'intègre donc pour obtenir pour obtenir une énergie en Wh
+ Energie_jour_Soutiree += ((float)delta_temps / 1000) * (PuissanceS_M_inst / 3600.0);
+ }
+
+ // PowerFactor_M = ((float)(Lecture333[53] * 256 + Lecture333[54])) / 1000; ce facteur de puissance ne veut rien dire en tri-phasé en cas d'injection sur au moins une phase
+
+ Energie_M_Soutiree = ((float)((float)(Lecture333[119] * 16777216) + (float)(Lecture333[120] * 65536) + (float)(Lecture333[121] * 256) + (float)Lecture333[122])) * 10;
+ Energie_M_Injectee = ((float)((float)(Lecture333[135] * 16777216) + (float)(Lecture333[136] * 65536) + (float)(Lecture333[137] * 256) + (float)Lecture333[138])) * 10;
+
+ MK333_dataBrute = "Triphasé
Phase1 : " + String(int(Tension_M1)) + "V " + String(Intensite_M1) + "A";
+ MK333_dataBrute += "
Phase2 : " + String(int(Tension_M2)) + "V " + String(Intensite_M2) + "A";
+ MK333_dataBrute += "
Phase3 : " + String(int(Tension_M3)) + "V " + String(Intensite_M3) + "A";
+ MK333_dataBrute += "
Puissance active soutirée : " + String(PuissanceS_M_inst) + "W";
+ MK333_dataBrute += "
Puissance active injectée : " + String(PuissanceI_M_inst) + "W";
+ MK333_dataBrute += "
Puissance apparente soutirée : " + String(PVAS_M_inst) + "VA";
+ MK333_dataBrute += "
Puissance apparente injectée : " + String(PVAI_M_inst) + "VA";
+
+ if (PVA_M_inst1 != 0)
+ MK333_dataBrute += "
Facteur de puissance phase 1 : " + String(abs(PW_inst1 / PVA_M_inst1)) + "";
+ if (PVA_M_inst2 != 0)
+ MK333_dataBrute += "
Facteur de puissance phase 2 : " + String(abs(PW_inst2 / PVA_M_inst2)) + "";
+ if (PVA_M_inst3 != 0)
+ MK333_dataBrute += "
Facteur de puissance phase 3 : " + String(abs(PW_inst3 / PVA_M_inst3)) + "";
+
+ MK333_dataBrute += "
Energie jour nette soutirée (Linky): " + String(Energie_jour_Soutiree) + "Wh";
+ MK333_dataBrute += "
Energie jour nette injectée (Linky): " + String(Energie_jour_Injectee) + "Wh";
+
+ MK333_dataBrute += "
Energie totale soutirée : " + String(Energie_M_Soutiree) + "Wh";
+ MK333_dataBrute += "
Energie totale injectée : " + String(Energie_M_Injectee) + "Wh";
+
+ Pva_valide = true;
+ filtre_puissance();
+ ComOK(); //Reset du Watchdog à chaque trame du JSY reçue
+ EnergieActiveValide = true;
+ if (cptLEDyellow > 30) {
+ cptLEDyellow = 4;
+ }
+ } else {
+ StockMessage("Pas tout reçu, pas traité... nombre de données : " + String(a));
+ }
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino
new file mode 100644
index 0000000..5c0b1a0
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino
@@ -0,0 +1,466 @@
+// ***************************
+// Stockage des données en ROM
+// ***************************
+//Plan stockage
+#define EEPROM_SIZE 4090
+#define NbJour 370 //Nb jour historique stocké
+#define adr_HistoAn 0 //taille 2* 370*4=1480
+#define adr_E_T_soutire0 1480 // 1 long. Taille 4 Triac
+#define adr_E_T_injecte0 1484
+#define adr_E_M_soutire0 1488 // 1 long. Taille 4 Maison
+#define adr_E_M_injecte0 1492 // 1 long. Taille 4
+#define adr_DateCeJour 1496 // String 8+1
+#define adr_lastStockConso 1505 // Short taille 2
+#define adr_ParaActions 1507 //Clé + ensemble parametres peu souvent modifiés
+
+
+void INIT_EEPROM(void) {
+ if (!EEPROM.begin(EEPROM_SIZE)) {
+ StockMessage("Failed to initialise EEPROM");
+ delay(10000);
+ ESP.restart();
+ }
+}
+
+void RAZ_Histo_Conso() {
+ //Mise a zero Zone stockage
+ int Adr_SoutInjec = adr_HistoAn;
+ for (int i = 0; i < NbJour; i++) {
+ EEPROM.writeLong(Adr_SoutInjec, 0);
+ Adr_SoutInjec = Adr_SoutInjec + 4;
+ }
+ EEPROM.writeULong(adr_E_T_soutire0, 0);
+ EEPROM.writeULong(adr_E_T_injecte0, 0);
+ EEPROM.writeULong(adr_E_M_soutire0, 0);
+ EEPROM.writeULong(adr_E_M_injecte0, 0);
+ EEPROM.writeString(adr_DateCeJour, "");
+ EEPROM.writeUShort(adr_lastStockConso, 0);
+ EEPROM.commit();
+}
+
+void LectureConsoMatinJour(void) {
+
+ Energie_jour_Soutiree = 0; // en Wh
+ Energie_jour_Injectee = 0; // en Wh
+
+ EAS_T_J0 = EEPROM.readULong(adr_E_T_soutire0); //Triac
+ EAI_T_J0 = EEPROM.readULong(adr_E_T_injecte0);
+ EAS_M_J0 = EEPROM.readULong(adr_E_M_soutire0); //Maison
+ EAI_M_J0 = EEPROM.readULong(adr_E_M_injecte0);
+ DateCeJour = EEPROM.readString(adr_DateCeJour);
+ idxPromDuJour = EEPROM.readUShort(adr_lastStockConso);
+ if (Energie_T_Soutiree < EAS_T_J0) {
+ Energie_T_Soutiree = EAS_T_J0;
+ }
+ if (Energie_T_Injectee < EAI_T_J0) {
+ Energie_T_Injectee = EAI_T_J0;
+ }
+ if (Energie_M_Soutiree < EAS_M_J0) {
+ Energie_M_Soutiree = EAS_M_J0;
+ }
+ if (Energie_M_Injectee < EAI_M_J0) {
+ Energie_M_Injectee = EAI_M_J0;
+ }
+}
+
+
+void JourHeureChange() {
+ if (DATEvalid) {
+ //Time Update / de l'heure
+ time_t timestamp = time(NULL);
+ char buffer[MAX_SIZE_T];
+ struct tm *pTime = localtime(×tamp);
+ strftime(buffer, MAX_SIZE_T, "%d/%m/%Y %H:%M:%S", pTime);
+ DATE = String(buffer);
+ strftime(buffer, MAX_SIZE_T, "%d%m%Y", pTime);
+ String JourCourant = String(buffer);
+ strftime(buffer, MAX_SIZE_T, "%Y-%m-%d", pTime);
+ DateEDF = String(buffer);
+ strftime(buffer, MAX_SIZE_T, "%H", pTime);
+ int hour = String(buffer).toInt();
+ strftime(buffer, MAX_SIZE_T, "%M", pTime);
+ int minute = String(buffer).toInt();
+ strftime(buffer, MAX_SIZE_T, "%s", pTime);
+ unsigned long Tactu = String(buffer).toInt();
+ if (T0_seconde == 0) T0_seconde = Tactu;
+ T_On_seconde = Tactu - T0_seconde;
+ HeureCouranteDeci = hour * 100 + minute * 10 / 6;
+ if (DateCeJour != JourCourant) { //Changement de jour
+ if (EnergieActiveValide && DateCeJour != "") { //Données recues
+ idxPromDuJour = (idxPromDuJour + 1 + NbJour) % NbJour;
+ //On enregistre les conso en début de journée pour l'historique de l'année
+ long energie = Energie_M_Soutiree - Energie_M_Injectee; //Bilan energie du jour
+ EEPROM.writeLong(idxPromDuJour * 4, energie);
+ EEPROM.writeULong(adr_E_T_soutire0, long(Energie_T_Soutiree));
+ EEPROM.writeULong(adr_E_T_injecte0, long(Energie_T_Injectee));
+ EEPROM.writeULong(adr_E_M_soutire0, long(Energie_M_Soutiree));
+ EEPROM.writeULong(adr_E_M_injecte0, long(Energie_M_Injectee));
+ EEPROM.writeString(adr_DateCeJour, JourCourant);
+ EEPROM.writeUShort(adr_lastStockConso, idxPromDuJour);
+ EEPROM.commit();
+ LectureConsoMatinJour();
+ }
+ DateCeJour = JourCourant;
+ }
+ }
+}
+String HistoriqueEnergie1An(void) {
+ String S = "";
+ int Adr_SoutInjec = 0;
+ long EnergieJour = 0;
+ long DeltaEnergieJour = 0;
+ int iS = 0;
+ long lastDay = 0;
+
+ for (int i = 0; i < NbJour; i++) {
+ iS = (idxPromDuJour + i + 1) % NbJour;
+ Adr_SoutInjec = adr_HistoAn + iS * 4;
+ EnergieJour = EEPROM.readLong(Adr_SoutInjec);
+ if (lastDay == 0) { lastDay = EnergieJour; }
+ DeltaEnergieJour = EnergieJour - lastDay;
+ lastDay = EnergieJour;
+ S += String(DeltaEnergieJour) + ",";
+ }
+ return S;
+}
+unsigned long LectureCle() {
+ return EEPROM.readULong(adr_ParaActions);
+}
+void LectureEnROM() {
+ int Hdeb;
+ int address = adr_ParaActions;
+ int VersionStocke;
+ Cle_ROM = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ VersionStocke = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ ssid = EEPROM.readString(address);
+ address += ssid.length() + 1;
+ password = EEPROM.readString(address);
+ address += password.length() + 1;
+ dhcpOn = EEPROM.readByte(address);
+ address += sizeof(byte);
+ IP_Fixe = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ Gateway = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ masque = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ dns = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ Source = EEPROM.readString(address);
+ address += Source.length() + 1;
+ RMSextIP = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ EnphaseUser = EEPROM.readString(address);
+ address += EnphaseUser.length() + 1;
+ EnphasePwd = EEPROM.readString(address);
+ address += EnphasePwd.length() + 1;
+ EnphaseSerial = EEPROM.readString(address);
+ address += EnphaseSerial.length() + 1;
+ MQTTRepet = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ MQTTIP = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ MQTTPort = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ MQTTUser = EEPROM.readString(address);
+ address += MQTTUser.length() + 1;
+ MQTTPwd = EEPROM.readString(address);
+ address += MQTTPwd.length() + 1;
+ MQTTPrefix = EEPROM.readString(address);
+ address += MQTTPrefix.length() + 1;
+ MQTTdeviceName = EEPROM.readString(address);
+ address += MQTTdeviceName.length() + 1;
+ TopicP = EEPROM.readString(address);
+ address += TopicP.length() + 1;
+ TopicT = EEPROM.readString(address);
+ address += TopicT.length() + 1;
+ subMQTT = EEPROM.readByte(address);
+ address += sizeof(byte);
+ nomRouteur = EEPROM.readString(address);
+ address += nomRouteur.length() + 1;
+ nomSondeFixe = EEPROM.readString(address);
+ address += nomSondeFixe.length() + 1;
+ nomSondeMobile = EEPROM.readString(address);
+ address += nomSondeMobile.length() + 1;
+ nomTemperature = EEPROM.readString(address);
+ address += nomTemperature.length() + 1;
+ Source_Temp = EEPROM.readString(address);
+ address += Source_Temp.length() + 1;
+ IPtemp = EEPROM.readULong(address);
+ address += sizeof(unsigned long);
+ CalibU = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ CalibI = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ TempoEDFon = EEPROM.readByte(address);
+ address += sizeof(byte);
+ WifiSleep = EEPROM.readByte(address);
+ address += sizeof(byte);
+ pSerial = EEPROM.readByte(address);
+ address += sizeof(byte);
+ pTriac = EEPROM.readByte(address);
+ address += sizeof(byte);
+
+ address += 100; //RĂ©serve de 100 bytes
+
+ //Zone des actions
+ NbActions = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ for (int iAct = 0; iAct < NbActions; iAct++) {
+ LesActions[iAct].Actif = EEPROM.readByte(address);
+ address += sizeof(byte);
+ LesActions[iAct].Titre = EEPROM.readString(address);
+ address += LesActions[iAct].Titre.length() + 1;
+ LesActions[iAct].Host = EEPROM.readString(address);
+ address += LesActions[iAct].Host.length() + 1;
+ LesActions[iAct].Port = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].OrdreOn = EEPROM.readString(address);
+ address += LesActions[iAct].OrdreOn.length() + 1;
+ LesActions[iAct].OrdreOff = EEPROM.readString(address);
+ address += LesActions[iAct].OrdreOff.length() + 1;
+ LesActions[iAct].Repet = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Tempo = EEPROM.readUShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Reactivite = EEPROM.readByte(address);
+ address += sizeof(byte);
+ address += 40; //RĂ©serve de 40 bytes
+ LesActions[iAct].NbPeriode = EEPROM.readByte(address);
+ address += sizeof(byte);
+ Hdeb = 0;
+ for (byte i = 0; i < LesActions[iAct].NbPeriode; i++) {
+ LesActions[iAct].Type[i] = EEPROM.readByte(address);
+ address += sizeof(byte);
+ LesActions[iAct].Hfin[i] = EEPROM.readUShort(address);
+ LesActions[iAct].Hdeb[i] = Hdeb;
+ Hdeb = LesActions[iAct].Hfin[i];
+ address += sizeof(unsigned short);
+ LesActions[iAct].Vmin[i] = EEPROM.readShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Vmax[i] = EEPROM.readShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Tinf[i] = EEPROM.readShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Tsup[i] = EEPROM.readShort(address);
+ address += sizeof(unsigned short);
+ LesActions[iAct].Tarif[i] = EEPROM.readByte(address);
+ address += sizeof(byte);
+ address += 10; //RĂ©serve de 10 bytes
+ }
+ }
+ Calibration(address);
+
+}
+int EcritureEnROM() {
+ int address = adr_ParaActions;
+ int VersionStocke = 0;
+ String V=Version;
+ VersionStocke =int(100*V.toFloat());
+ EEPROM.writeULong(address, Cle_ROM);
+ address += sizeof(unsigned long);
+ EEPROM.writeUShort(address, VersionStocke);
+ address += sizeof(unsigned short);
+ EEPROM.writeString(address, ssid);
+ address += ssid.length() + 1;
+ EEPROM.writeString(address, password);
+ address += password.length() + 1;
+ EEPROM.writeByte(address, dhcpOn);
+ address += sizeof(byte);
+ EEPROM.writeULong(address, IP_Fixe);
+ address += sizeof(unsigned long);
+ EEPROM.writeULong(address, Gateway);
+ address += sizeof(unsigned long);
+ EEPROM.writeULong(address, masque);
+ address += sizeof(unsigned long);
+ EEPROM.writeULong(address, dns);
+ address += sizeof(unsigned long);
+ EEPROM.writeString(address, Source);
+ address += Source.length() + 1;
+ EEPROM.writeULong(address, RMSextIP);
+ address += sizeof(unsigned long);
+ EEPROM.writeString(address, EnphaseUser);
+ address += EnphaseUser.length() + 1;
+ EEPROM.writeString(address, EnphasePwd);
+ address += EnphasePwd.length() + 1;
+ EEPROM.writeString(address, EnphaseSerial);
+ address += EnphaseSerial.length() + 1;
+ EEPROM.writeUShort(address, MQTTRepet);
+ address += sizeof(unsigned short);
+ EEPROM.writeULong(address, MQTTIP);
+ address += sizeof(unsigned long);
+ EEPROM.writeUShort(address, MQTTPort);
+ address += sizeof(unsigned short);
+ EEPROM.writeString(address, MQTTUser);
+ address += MQTTUser.length() + 1;
+ EEPROM.writeString(address, MQTTPwd);
+ address += MQTTPwd.length() + 1;
+ EEPROM.writeString(address, MQTTPrefix);
+ address += MQTTPrefix.length() + 1;
+ EEPROM.writeString(address, MQTTdeviceName);
+ address += MQTTdeviceName.length() + 1;
+ EEPROM.writeString(address, TopicP);
+ address += TopicP.length() + 1;
+ EEPROM.writeString(address, TopicT);
+ address += TopicT.length() + 1;
+ EEPROM.writeByte(address, subMQTT);
+ address += sizeof(byte);
+ EEPROM.writeString(address, nomRouteur);
+ address += nomRouteur.length() + 1;
+ EEPROM.writeString(address, nomSondeFixe);
+ address += nomSondeFixe.length() + 1;
+ EEPROM.writeString(address, nomSondeMobile);
+ address += nomSondeMobile.length() + 1;
+ EEPROM.writeString(address, nomTemperature);
+ address += nomTemperature.length() + 1;
+ EEPROM.writeString(address, Source_Temp);
+ address += Source_Temp.length() + 1;
+ EEPROM.writeULong(address, IPtemp);
+ address += sizeof(unsigned long);
+ EEPROM.writeUShort(address, CalibU);
+ address += sizeof(unsigned short);
+ EEPROM.writeUShort(address, CalibI);
+ address += sizeof(unsigned short);
+ EEPROM.writeByte(address, TempoEDFon);
+ address += sizeof(byte);
+ EEPROM.writeByte(address, WifiSleep);
+ address += sizeof(byte);
+ EEPROM.writeByte(address, pSerial);
+ address += sizeof(byte);
+ EEPROM.writeByte(address, pTriac);
+ address += sizeof(byte);
+
+ address += 100; //RĂ©serve de 100 bytes
+
+ //Enregistrement des Actions
+ EEPROM.writeUShort(address, NbActions);
+ address += sizeof(unsigned short);
+ for (int iAct = 0; iAct < NbActions; iAct++) {
+ EEPROM.writeByte(address, LesActions[iAct].Actif);
+ address += sizeof(byte);
+ EEPROM.writeString(address, LesActions[iAct].Titre);
+ address += LesActions[iAct].Titre.length() + 1;
+ EEPROM.writeString(address, LesActions[iAct].Host);
+ address += LesActions[iAct].Host.length() + 1;
+ EEPROM.writeUShort(address, LesActions[iAct].Port);
+ address += sizeof(unsigned short);
+ EEPROM.writeString(address, LesActions[iAct].OrdreOn);
+ address += LesActions[iAct].OrdreOn.length() + 1;
+ EEPROM.writeString(address, LesActions[iAct].OrdreOff);
+ address += LesActions[iAct].OrdreOff.length() + 1;
+ EEPROM.writeUShort(address, LesActions[iAct].Repet);
+ address += sizeof(unsigned short);
+ EEPROM.writeUShort(address, LesActions[iAct].Tempo);
+ address += sizeof(unsigned short);
+ EEPROM.writeByte(address, LesActions[iAct].Reactivite);
+ address += sizeof(byte);
+ address += 40; //RĂ©serve de 40 bytes
+ EEPROM.writeByte(address, LesActions[iAct].NbPeriode);
+ address += sizeof(byte);
+ for (byte i = 0; i < LesActions[iAct].NbPeriode; i++) {
+ EEPROM.writeByte(address, LesActions[iAct].Type[i]);
+ address += sizeof(byte);
+ EEPROM.writeUShort(address, LesActions[iAct].Hfin[i]);
+ address += sizeof(unsigned short);
+ EEPROM.writeShort(address, LesActions[iAct].Vmin[i]);
+ address += sizeof(unsigned short);
+ EEPROM.writeShort(address, LesActions[iAct].Vmax[i]);
+ address += sizeof(unsigned short);
+ EEPROM.writeShort(address, LesActions[iAct].Tinf[i]);
+ address += sizeof(unsigned short);
+ EEPROM.writeShort(address, LesActions[iAct].Tsup[i]);
+ address += sizeof(unsigned short);
+ EEPROM.writeByte(address, LesActions[iAct].Tarif[i]);
+ address += sizeof(byte);
+ address += 10; //RĂ©serve de 10 bytes
+ }
+ }
+ Calibration(address);
+ EEPROM.commit();
+ return address;
+}
+void Calibration(int address) {
+ kV = KV * CalibU / 1000; //Calibration coefficient to be applied
+ kI = KI * CalibI / 1000;
+ P_cent_EEPROM = int(100 * address / EEPROM_SIZE);
+ Serial.println("Mémoire EEPROM utilisée : " + String(P_cent_EEPROM) + "%");
+}
+
+void init_puissance() {
+ PuissanceS_T = 0;
+ PuissanceS_M = 0;
+ PuissanceI_T = 0;
+ PuissanceI_M = 0; //Puissance Watt affichée en entiers Maison et Triac
+ PVAS_T = 0;
+ PVAS_M = 0;
+ PVAI_T = 0;
+ PVAI_M = 0; //Puissance VA affichée en entiers Maison et Triac
+ PuissanceS_T_inst = 0.0;
+ PuissanceS_M_inst = 0.0;
+ PuissanceI_T_inst = 0.0;
+ PuissanceI_M_inst = 0.0;
+ PVAS_T_inst = 0.0;
+ PVAS_M_inst = 0.0;
+ PVAI_T_inst = 0.0;
+ PVAI_M_inst = 0.0;
+ Puissance_T_moy = 0.0;
+ Puissance_M_moy = 0.0;
+ PVA_T_moy = 0.0;
+ PVA_M_moy = 0.0;
+}
+void filtre_puissance() { //Filtre RC
+
+ float A = 0.3; //Coef pour un lissage en multi-sinus et train de sinus sur les mesures de puissance courte
+ float B = 0.7;
+ if (!LissageLong) {
+ A = 1;
+ B = 0;
+ }
+
+ Puissance_T_moy = A * (PuissanceS_T_inst - PuissanceI_T_inst) + B * Puissance_T_moy;
+ if (Puissance_T_moy < 0) {
+ PuissanceI_T = -int(Puissance_T_moy); //Puissance Watt affichée en entier Triac
+ PuissanceS_T = 0;
+ } else {
+ PuissanceS_T = int(Puissance_T_moy);
+ PuissanceI_T = 0;
+ }
+
+
+ Puissance_M_moy = A * (PuissanceS_M_inst - PuissanceI_M_inst) + B * Puissance_M_moy;
+ if (Puissance_M_moy < 0) {
+ PuissanceI_M = -int(Puissance_M_moy); //Puissance Watt affichée en entier Maison
+ PuissanceS_M = 0;
+ } else {
+ PuissanceS_M = int(Puissance_M_moy);
+ PuissanceI_M = 0;
+ }
+
+
+ PVA_T_moy = A * (PVAS_T_inst - PVAI_T_inst) + B * PVA_T_moy; //Puissance VA affichée en entiers
+ if (PVA_T_moy < 0) {
+ PVAI_T = -int(PVA_T_moy);
+ PVAS_T = 0;
+ } else {
+ PVAS_T = int(PVA_T_moy);
+ PVAI_T = 0;
+ }
+
+ PVA_M_moy = A * (PVAS_M_inst - PVAI_M_inst) + B * PVA_M_moy;
+ if (PVA_M_moy < 0) {
+ PVAI_M = -int(PVA_M_moy);
+ PVAS_M = 0;
+ } else {
+ PVAS_M = int(PVA_M_moy);
+ PVAI_M = 0;
+ }
+}
+
+void StockMessage(String m) {
+ m = DATE + " : " + m;
+ Serial.println(m);
+ MessageH[idxMessage] = m;
+ idxMessage = (idxMessage + 1) % 10;
+}
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino
new file mode 100644
index 0000000..aa5d9cc
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino
@@ -0,0 +1,91 @@
+// ***************
+// * Temperature *
+// ***************
+void LectureTemperature() {
+ float temperature_brute = -127;
+ if (Source_Temp == "tempNo") {
+ temperature = temperature_brute;
+ }
+ if (Source_Temp == "tempInt") {
+ if (!ds18b20_Init) {
+ ds18b20_Init = true;
+ ds18b20.begin();
+ }
+ ds18b20.requestTemperatures();
+ temperature_brute = ds18b20.getTempCByIndex(0);
+ if (temperature_brute < -20 || temperature_brute > 130) { //Invalide. Pas de capteur ou parfois mauvaise réponse
+ if (TemperatureValide > 0) {
+ TemperatureValide = TemperatureValide - 1; // Perte Ă©ventuels de quelques mesures
+ } else {
+ StockMessage("Mesure Température invalide ou pas de capteur DS18B20"); //Trop de pertes
+ temperature = temperature_brute;
+ }
+ } else {
+ TemperatureValide = 5;
+ temperature = temperature_brute;
+ }
+ }
+ if (Source_Temp == "tempExt") {
+ String RMSExtTemp = "";
+
+ // Use WiFiClient class to create TCP connections
+ WiFiClient clientESP_RMS;
+ byte arr[4];
+ arr[0] = IPtemp & 0xFF;
+ arr[1] = (IPtemp >> 8) & 0xFF;
+ arr[2] = (IPtemp >> 16) & 0xFF;
+ arr[3] = (IPtemp >> 24) & 0xFF;
+
+ String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]);
+ if (!clientESP_RMS.connect(host.c_str(), 80)) {
+ StockMessage("connection to ESP_RMS Temperature failed : " + host);
+ delay(200);
+ ComAbuge();
+ if (TemperatureValide > 0) {
+ TemperatureValide = TemperatureValide - 1; // Perte Ă©ventuels de quelques mesures
+ } else { //Trop de pertes
+ temperature = temperature_brute;
+ }
+ return;
+ }
+ String url = "/ajax_Temperature";
+ clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
+ unsigned long timeout = millis();
+ while (clientESP_RMS.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage("client ESP_RMS Temperature Timeout !" + host);
+ clientESP_RMS.stop();
+ if (TemperatureValide > 0) {
+ TemperatureValide = TemperatureValide - 1; // Perte Ă©ventuels de quelques mesures
+ } else { //Trop de pertes
+ temperature = temperature_brute;
+ }
+ return;
+ }
+ }
+ timeout = millis();
+ // Lecture des données brutes distantes
+ while (clientESP_RMS.available() && (millis() - timeout < 5000)) {
+ RMSExtTemp += clientESP_RMS.readStringUntil('\r');
+ }
+ if (RMSExtTemp.length() > 100) {
+ RMSExtTemp = "";
+ }
+ if (RMSExtTemp.indexOf(GS) >= 0 && RMSExtTemp.indexOf(RS) > 0) { //Trame complète reçue
+ RMSExtTemp = RMSExtTemp.substring(RMSExtTemp.indexOf(GS) + 1);
+ RMSExtTemp = RMSExtTemp.substring(0, RMSExtTemp.indexOf(RS));
+ temperature_brute = RMSExtTemp.toFloat();
+ RMSExtTemp = "";
+ TemperatureValide =5;
+ temperature = temperature_brute;
+ }
+
+ }
+ if (Source_Temp == "tempMqtt") {
+ if (TemperatureValide > 0) {
+ TemperatureValide = TemperatureValide - 1; // Watchdog pour verfier mesures arrivent voir MQTT.ino
+ } else {
+ temperature = temperature_brute;
+ }
+ }
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino
new file mode 100644
index 0000000..f2be49e
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino
@@ -0,0 +1,78 @@
+// *******************************************************
+// * Recherche Info Tempo EDF pour les sources non Linky *
+// *******************************************************
+
+
+void Call_EDF_data() {
+
+ const char* adr_EDF_Host = "particulier.edf.fr";
+ String Host = String(adr_EDF_Host);
+ String urlJSON = "/services/rest/referentiel/searchTempoStore?dateRelevant=" + DateEDF;
+ String EDFdata = "";
+ String line = "";
+ int Hcour = HeureCouranteDeci / 2; //Par pas de 72secondes pour faire 2 appels si un bug
+ int LastH = LastHeureEDF / 2;
+
+ if ((LastH != Hcour) && ( Hcour == 300 || Hcour == 310 || Hcour == 530 || Hcour == 560 || Hcour == 600 || Hcour == 900 || Hcour == 1150) || LastHeureEDF < 0) {
+ if (TempoEDFon == 1) {
+ // Use clientSecu class to create TCP connections
+ clientSecuEDF.setInsecure(); //skip verification
+ if (!clientSecuEDF.connect(adr_EDF_Host, 443)) {
+ StockMessage("Connection failed to EDF server :" + Host);
+ } else {
+ String Request=String("GET ") + urlJSON + " HTTP/1.1\r\n" ;
+ Request += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" ;
+ Request += "Accept-Encoding: gzip, deflate, br, zstd\r\n" ;
+ Request += "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\n" ;
+ Request += "Host: " + Host + "\r\n" ;
+ Request += "Connection: keep-alive\r\n\r\n";
+ clientSecuEDF.print(Request);
+ Serial.println("Request vers EDF Envoyé");
+ unsigned long timeout = millis();
+ while (clientSecuEDF.available() == 0) {
+ if (millis() - timeout > 5000) {
+ StockMessage(">>> clientSecuEDF EDF Timeout !");
+ clientSecuEDF.stop();
+ return;
+ }
+ }
+ timeout = millis();
+ // Lecture des données brutes distantes
+ int fin = 0;
+ while (clientSecuEDF.connected() && (millis() - timeout < 5000) && fin < 2) {
+ line = clientSecuEDF.readStringUntil('\n');
+ EDFdata += line;
+ if (line == "\r") {
+ StockMessage("EnTetes EDF reçues");
+ EDFdata = "";
+ fin = 1;
+ }
+ if (fin == 1 && line.indexOf("}") >= 0) fin = 2;
+ }
+ clientSecuEDF.stop();
+
+ // C'est EDF qui donne la couleur
+ String LTARFrecu = StringJson("couleurJourJ", EDFdata); //Remplace code du Linky
+ if (LTARFrecu.indexOf("TEMPO") >= 0) {
+ LTARF = LTARFrecu;
+ String couleurJourJ1 = StringJson("couleurJourJ1", EDFdata);
+ line = "0";
+ if (couleurJourJ1 == "TEMPO_BLEU") line = "4";
+ if (couleurJourJ1 == "TEMPO_BLANC") line = "8";
+ if (couleurJourJ1 == "TEMPO_ROUGE") line = "C";
+ STGE = line; //Valeur Hexa code du Linky
+ StockMessage(DateEDF + " : " + EDFdata);
+ EDFdata = "";
+ LastHeureEDF = HeureCouranteDeci; //Heure lecture Tempo EDF
+ } else {
+ StockMessage(DateEDF + " : Pas de données EDF valides");
+ }
+ }
+ } else {
+ if (Source != "Linky" && Source != "Ext") {
+ LTARF = "";
+ STGE = "0";
+ }
+ }
+ }
+}
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h
new file mode 100644
index 0000000..46b9869
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h
@@ -0,0 +1,665 @@
+//************************************************
+// Page HTML et Javascript de gestion des Actions
+//************************************************
+
+const char *ActionsHtml = R"====(
+
+
+
+
+
+
+
+
+
+
Routeur Solaire - RMS
Planning des Routages (suivant sonde Maison)
+
Routage via Triac
+
+
Routage via Relais
+
+
+
+
+
+
+)====";
+const char *ActionsJS = R"====(
+ var LesActions = [];
+ var mouseClick = false;
+ var blockEvent = false;
+ var temperatureDS=-127;
+ var LTARFbin=0;
+ var pTriac=0;
+ var IS=String.fromCharCode(31); //Input Separator
+ function Init() {
+ LoadActions();
+ DispTimer();
+ LoadParaRouteur();
+ }
+ function creerAction(aActif, aTitre, aHost, aPort, aOrdreOn, aOrdreOff, aRepet,aTempo,aReactivite, aPeriodes) {
+ var S = {
+ Actif: aActif,
+ Titre: aTitre,
+ Host: aHost,
+ Port: aPort,
+ OrdreOn: aOrdreOn,
+ OrdreOff: aOrdreOff,
+ Repet: aRepet,
+ Tempo: aTempo,
+ Reactivite: aReactivite,
+ Periodes: aPeriodes
+ }
+ return S;
+ }
+ function TracePlanning(iAct) {
+ var Radio0 = "Inactif
";
+ var Radio1 = "DĂ©coupe sinus
";
+ if (iAct > 0){Radio1 = "On/Off
";}
+ Radio1 += "Multi-sinus
";
+ Radio1 += "Train de sinus
";
+ var Pins=[0,4,5,14,16,17,21,22,23,25,26,27,-1];
+ var SelectPin="Gpio ";
+ var SelectOut=" Sortie 'On' ";
+ var S = "Titre
";
+ S +="" +Radio0 + Radio1 + "
";
+
+ S +="";
+ S += "
";
+
+ S +="
";
+ S +="
";
+ S +="
";
+ S +="
Réactivité lente ou charge importante
";
+ S +="
";
+ S +="
Réactivité rapide ou charge faible
";
+ S +="
";
+ S +="
";
+ S += "
";
+ S += "
";
+ S += "
";
+
+ GH("planning" + iAct, S);
+ GID("radio" + iAct +"-" +LesActions[iAct].Actif).checked = true;
+ GH("titre" + iAct, LesActions[iAct].Titre);
+ GV("host" + iAct, LesActions[iAct].Host);
+ GV("port" + iAct, LesActions[iAct].Port);
+ GV("ordreOn" + iAct, LesActions[iAct].OrdreOn);
+ GV("ordreOff" + iAct, LesActions[iAct].OrdreOff);
+ GV("repet" + iAct, LesActions[iAct].Repet);
+ GV("tempo" + iAct, LesActions[iAct].Tempo);
+ GV("slider" + iAct ,LesActions[iAct].Reactivite);
+ GH("sensi" + iAct ,LesActions[iAct].Reactivite);
+ if(LesActions[iAct].OrdreOn.indexOf(IS)>0){
+ var vals=LesActions[iAct].OrdreOn.split(IS);
+ GID("selectPin"+iAct).value=vals[0];
+ GID("SelectOut" + iAct).value=vals[1];
+ } else {
+ GID("selectPin"+iAct).value=-1;
+ GID("SelectOut" + iAct).value=1;
+ if(LesActions[iAct].OrdreOn=="") GID("selectPin"+iAct).value=0;
+ }
+ TracePeriodes(iAct);
+
+ }
+ function TracePeriodes(iAct) {
+ var S = "";
+ var Sinfo = "";
+ var left = 0;
+ var H0 = 0;
+ var colors = ["#666", "#66f", "#f66", "#6f6", "#cc4"]; //NO,OFF,ON,PW,Triac
+ blockEvent = false;
+ for (var i = 0; i < LesActions[iAct].Periodes.length; i++) {
+ var w = (LesActions[iAct].Periodes[i].Hfin - H0) /24;
+ left = H0 / 24;
+ H0 = LesActions[iAct].Periodes[i].Hfin;
+ var Type = LesActions[iAct].Periodes[i].Type;
+ var color = colors[Type];
+ var temperature="";
+ if (temperatureDS>-100) { // La sonde de température fonctionne
+ var Tsup=LesActions[iAct].Periodes[i].Tsup;
+ if (Tsup>=0 && Tsup <=1000) temperature +=" si T ≥" + Tsup/10 + "°
";
+ var Tinf=LesActions[iAct].Periodes[i].Tinf;
+ if (Tinf>=0 && Tinf <=1000) temperature +=" si T ≤" + Tinf/10 + "°
";
+ }
+ var TxtTarif= "";
+ if (LTARFbin>0) {
+ TxtTarif= " si Tarif : ";
+ var Tarif_=LesActions[iAct].Periodes[i].Tarif;
+ if (LTARFbin<=3) {
+ TxtTarif += (Tarif_ & 1) ? "H. Pleine":"" ;
+ TxtTarif += (Tarif_ & 2) ? " H. Creuse":"" ;
+ } else {
+ TxtTarif += (Tarif_ & 4) ? "TempoBleu":"" ;
+ TxtTarif += (Tarif_ & 8) ? " Blanc":"" ;
+ TxtTarif += (Tarif_ & 16) ? " Rouge":"" ;
+ }
+ TxtTarif ="" + TxtTarif +"
";
+ }
+ if (LesActions[iAct].Actif<=1 && iAct>0){
+ LesActions[iAct].Periodes[i].Vmax=Math.max(LesActions[iAct].Periodes[i].Vmin,LesActions[iAct].Periodes[i].Vmax);
+ var TexteMinMax="Off si Pw>"+LesActions[iAct].Periodes[i].Vmax+"W
On si Pw<"+LesActions[iAct].Periodes[i].Vmin+"W
"+temperature + TxtTarif;
+ } else {
+ LesActions[iAct].Periodes[i].Vmax=Math.max(0,LesActions[iAct].Periodes[i].Vmax);
+ LesActions[iAct].Periodes[i].Vmax=Math.min(100,LesActions[iAct].Periodes[i].Vmax);
+ var TexteMinMax="Seuil Pw : "+LesActions[iAct].Periodes[i].Vmin+"W
"+ temperature + "Ouvre Max : "+LesActions[iAct].Periodes[i].Vmax+"%
" + TxtTarif;
+ }
+ var TexteTriac="Seuil Pw : "+LesActions[iAct].Periodes[i].Vmin+"W
"+temperature + "Ouvre Max : "+LesActions[iAct].Periodes[i].Vmax+"%
"+TxtTarif;
+ var paras = ["Pas de contrôle", "OFF", "ON" + temperature + TxtTarif, TexteMinMax, TexteTriac];
+ var para = paras[Type];
+ S += "";
+ Hmn = Math.floor(H0 / 100) + ":" + ("0" + Math.floor(0.6 * (H0 - 100 * Math.floor(H0 / 100)))).substr(-2, 2);
+ fs = Math.max(8, Math.min(16, w/2)) + "px";
+ Sinfo += ""
+ Sinfo += "
" + Hmn + "
" + para + "
";
+ }
+ GH("curseurs" + iAct, S);
+ GH("infoAction" + iAct, Sinfo);
+ }
+ function touchMove(t, ev, iAct) {
+ var leftPos = ev.touches[0].clientX - GID(t.id).getBoundingClientRect().left;
+ NewPosition(t, leftPos, iAct);
+ }
+ function mouseMove(t, ev, iAct) {
+ if (mouseClick) {
+ var leftPos = ev.clientX - GID(t.id).getBoundingClientRect().left;
+ NewPosition(t, leftPos, iAct);
+ }
+ }
+ function NewPosition(t, leftPos, iAct) {
+ var G = GID(t.id).style.left;
+ //+ window.scrollX;
+ var width = GID(t.id).getBoundingClientRect().width;
+ var HeureMouse = leftPos * 2420 / width;
+ var idxClick = 0;
+ var deltaX = 999999;
+ for (var i = 0; i < LesActions[iAct].Periodes.length - 1; i++) {
+ var dist = Math.abs(HeureMouse - LesActions[iAct].Periodes[i].Hfin)
+ if (dist < deltaX) {
+ idxClick = i;
+ deltaX = dist;
+ }
+ }
+ var NewHfin = Math.max(0, Math.min(HeureMouse, 2400));
+ if (idxClick == LesActions[iAct].Periodes.length - 1) NewHfin=2400;
+ if (idxClick < LesActions[iAct].Periodes.length - 1)
+ NewHfin = Math.min(NewHfin, LesActions[iAct].Periodes[idxClick + 1].Hfin);
+ if (idxClick > 0)
+ NewHfin = Math.max(NewHfin, LesActions[iAct].Periodes[idxClick - 1].Hfin);
+ LesActions[iAct].Periodes[idxClick].Hfin = Math.floor(NewHfin);
+ TracePeriodes(iAct);
+
+ }
+ function AddSub(v, iAct) {
+ if (v == 1) {
+ if (LesActions[iAct].Periodes.length<8){
+ LesActions[iAct].Periodes.push({
+ Hfin: 2400,
+ Type: 1,
+ Vmin:0,
+ Vmax:100,
+ Tinf:1500,
+ Tsup:1500,
+ Tarif:31
+ }); //Tarif codé en bits
+ var Hbas = 0;
+ if (LesActions[iAct].Periodes.length > 2){
+ Hbas = parseInt(LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 3].Hfin);
+ }
+ if (LesActions[iAct].Periodes.length > 1) {
+ LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 2].Hfin = Math.floor((Hbas + 2400) / 2);
+ }
+ }
+ } else {
+ if (LesActions[iAct].Periodes.length>1){
+ LesActions[iAct].Periodes.pop();
+ if (LesActions[iAct].Periodes.length > 0)
+ LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 1].Hfin = 2400;
+ }
+ }
+ TracePeriodes(iAct);
+
+ }
+ function infoZclicK(i, iAct) {
+ if (!blockEvent) {
+ blockEvent = true;
+ var Type = LesActions[iAct].Periodes[i].Type;
+ var idZ = "info" + iAct + "Z" + i;
+ var S = "";
+ //On ne traite plus depuis version8 le cas "Pas de ContrĂ´le". Inutile
+ c = (Type == 1) ? "bInset" : "bOutset";
+ S += "OFF
";
+ S += "";
+ c = (Type == 2) ? "bInset" : "bOutset";
+ S += "
ON
";
+ c = (Type > 2) ? "bInset" : "bOutset";
+ var Vmin=LesActions[iAct].Periodes[i].Vmin;
+ var Vmax=LesActions[iAct].Periodes[i].Vmax;
+ var Tinf=LesActions[iAct].Periodes[i].Tinf;
+ var Tsup=LesActions[iAct].Periodes[i].Tsup;
+ var TinfC=Tinf/10;
+ var TsupC=Tsup/10;
+ if (Tinf>1000 || Tinf<0) TinfC=""; //Temperature entre 0 et 100° représenté en dixième
+ if (Tsup>1000 || Tsup<0) TsupC=""; //Temperature entre 0 et 100
+ if (iAct > 0) {
+ var Routage=["","Routage ON/Off","Routage Multi-sinus","Routage Train de Sinus"];
+ S += "
" +Routage[LesActions[iAct].Actif] + "
";
+ if (LesActions[iAct].Actif<=1) {
+ S += "
On : Pw <W
";
+ S += "
Off : Pw >W
";
+ S += "
Puissance active en entrée de maison
";
+ } else {
+ S += "
Seuil Pw : W
";
+ S += "
Puissance active en entrée de maison
";
+ S += "
Ouvre Max : %
";
+ }
+
+ } else {
+ var Routage=["","Routage DĂ©coupe Sinus","Routage Multi-sinus","Routage Train de Sinus"];
+ S += "
" +Routage[LesActions[iAct].Actif] + "
";
+ S += "
Seuil Pw W
";
+ S += "
Puissance active en entrée de maison
";
+ S += "
Ouvre Max %
";
+ }
+ S += "
";
+ S += "";
+ if (temperatureDS>-100) {
+ S += "
";
+ S += "
Actif si température :
";
+ S += "
T ≥°
";
+ S += "
T ≤°
";
+ S += "
T en degré (0.0 à 100.0) ou laisser vide
";
+ S += "
";
+ }
+ if (LTARFbin>0) {
+
+ S += "
";
+ }
+ S += "
";
+ S += "";
+ GH(idZ, S);
+ var Tarif_=LesActions[iAct].Periodes[i].Tarif;
+ if (LTARFbin>0) {
+ if (LTARFbin<=3) {
+ GID("TarifPl_" + idZ).checked = (Tarif_ & 1) ? 1:0 ; // H Pleine
+ GID("TarifCr_" + idZ).checked = (Tarif_ & 2) ? 1:0 ;
+ } else {
+ GID("TarifBe_" + idZ).checked = (Tarif_ & 4) ? 1:0 ;
+ GID("TarifBa_" + idZ).checked = (Tarif_ & 8) ? 1:0 ;
+ GID("TarifRo_" + idZ).checked = (Tarif_ & 16) ? 1:0 ; //Rouge
+ }
+ }
+ GID(idZ).style.display = "block";
+ }
+ }
+ function infoZclose(idx) {
+ var champs=idx.split("info");
+ var idx=champs[1].split("Z");
+ S="TracePeriodes("+idx[0]+");"
+ setTimeout(S, 100);
+ }
+ function selectZ(T, i, iAct) {
+ if (LesActions[iAct].Periodes[i].Type != T) {
+ LesActions[iAct].Periodes[i].Type = T;
+ var idZ = "info" + iAct + "Z" + i;
+ if (T <= 2)
+ infoZclose(idZ);
+ TracePeriodes(iAct);
+ }
+ }
+)====";
+const char *ActionsJS2 = R"====(
+ function NewVal(t){
+ var champs=t.id.split("info");
+ var idx=champs[1].split("Z"); //Num Action, Num période
+ if (champs[0].indexOf("min")>0){
+ LesActions[idx[0]].Periodes[idx[1]].Vmin=Math.floor(GID(t.id).value);
+ }
+ if (champs[0].indexOf("max")>0){
+ LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.floor(GID(t.id).value);
+ if (idx[0]==0){
+ LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.max(LesActions[idx[0]].Periodes[idx[1]].Vmax,5);
+ LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.min(LesActions[idx[0]].Periodes[idx[1]].Vmax,100);
+ }
+ }
+ if (champs[0].indexOf("inf")>0){
+ var V= GID(t.id).value;
+ if (V=="") V=128;
+ LesActions[idx[0]].Periodes[idx[1]].Tinf=Math.floor(V*10);
+ }
+ if (champs[0].indexOf("sup")>0){
+ var V= GID(t.id).value;
+ if (V=="") V=128;
+ LesActions[idx[0]].Periodes[idx[1]].Tsup=Math.floor(V*10);
+ }
+
+ if (champs[0].indexOf("Tarif")>=0){
+ var idZ = "info" + champs[1];
+ var Tarif_ = 0;
+ if (LTARFbin<=3) {
+ Tarif_ += GID("TarifPl_" + idZ).checked ? 1:0; //H pleine
+ Tarif_ += GID("TarifCr_" + idZ).checked ? 2:0;
+ } else {
+ Tarif_ += GID("TarifBe_" + idZ).checked ? 4:0; //Bleu
+ Tarif_ += GID("TarifBa_" + idZ).checked ? 8:0;
+ Tarif_ += GID("TarifRo_" + idZ).checked ? 16:0; //Rouge
+ }
+ LesActions[idx[0]].Periodes[idx[1]].Tarif=Tarif_;
+ }
+ }
+ function editTitre(iAct) {
+ if (GID("titre" + iAct).innerHTML.indexOf("");
+ GID("Etitre" + iAct).focus();
+ }
+ }
+ function TitreValid(iAct) {
+ LesActions[iAct].Titre = GID("Etitre" + iAct).value.trim();
+ GH("titre" + iAct, LesActions[iAct].Titre);
+ }
+ function checkDisabled(){
+ for (var iAct = 0; iAct < LesActions.length; iAct++) {
+ for (var i=0;i<=3;i++){
+ if( GID("radio" + iAct +"-"+ i).checked ) { LesActions[iAct].Actif =i;}
+ }
+ TracePeriodes(iAct);
+ GID("planning0").style.display = (pTriac>0) ? "block" : "none"; // Si Pas de Triac
+ GID("TitrTriac").style.display = (pTriac>0) ? "block" : "none";
+ GID("blocPlanning"+iAct).style.display = (LesActions[iAct].Actif>0) ? "block" : "none";
+ var visible = ( LesActions[iAct].Actif== 1) ? "visible" : "hidden";
+ GID("Tempo"+iAct).style.visibility =visible;
+ GID("tempo"+iAct).style.visibility =visible;
+ var disable=true;
+ var disp="block";
+ if (GID("selectPin"+iAct).value>=0) { visible="hidden";disable=false;disp="none";}
+ GID("SelectOut"+iAct).style.display = (GID("selectPin"+iAct).value<=0) ? "none":"inline-block";
+ GID("Host"+iAct).style.visibility =visible;
+ GID("host"+iAct).style.visibility =visible;
+ GID("Port"+iAct).style.visibility =visible;
+ GID("port"+iAct).style.visibility =visible;
+ GID("Repet"+iAct).style.visibility =visible;
+ GID("repet"+iAct).style.visibility =visible;
+ GID("radio" + iAct +"-2").disabled = disable;
+ GID("radio" + iAct +"-3").disabled = disable;
+ GID("ordreoff"+iAct).style.display=disp;
+ GID("ordreon"+iAct).style.display =disp;
+ if (GID("selectPin"+iAct).value==-1 && GID("ordreOn"+iAct).value.indexOf(IS)>0) GID("ordreOn"+iAct).value="";
+ GID("ligne_bas"+iAct).style.display =( LesActions[iAct].Actif> 1) ? "none" :"table-row";
+ GID("fen_slide"+iAct).style.visibility = (LesActions[iAct].Actif== 1 && iAct>0 ) ? "hidden" : "visible";
+ }
+ }
+ function LoadActions() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function () {
+ if (this.readyState == 4 && this.status == 200) {
+ var LeRetour = this.responseText;
+ var Les_ACTIONS = LeRetour.split(GS);
+ var LesParas = Les_ACTIONS[0].split(RS);
+ temperatureDS=LesParas[0];
+ LTARFbin = parseInt(LesParas[1]);
+ pTriac = parseInt(LesParas[2]);
+ LesActions.splice(0,LesActions.length);
+ for (var iAct=1;iAct";
+ }
+ GH("plannings", S);
+ for (var iAct = 0; iAct < LesActions.length; iAct++) {
+ TracePlanning(iAct);
+ }
+ checkDisabled();
+
+ }
+ };
+ xhttp.open('GET', 'ActionsAjax', true);
+ xhttp.send();
+ }
+
+
+ function SendValues() {
+ GID("attente").style="visibility: visible;";
+ for (var iAct = 0; iAct < LesActions.length; iAct++) {
+ for (var i=0;i<=3;i++){
+ if( GID("radio" + iAct +"-"+ i).checked ) { LesActions[iAct].Actif =i;}
+ }
+ LesActions[iAct].Titre = GID("titre" + iAct).innerHTML.trim();
+ LesActions[iAct].Host = GID("host" + iAct).value.trim();
+ LesActions[iAct].Port = GID("port" + iAct).value;
+ LesActions[iAct].OrdreOn = GID("ordreOn" + iAct).value.trim();
+ LesActions[iAct].OrdreOff = GID("ordreOff" + iAct).value.trim();
+ LesActions[iAct].Repet = GID("repet" + iAct).value;
+ LesActions[iAct].Tempo = GID("tempo" + iAct).value;
+ LesActions[iAct].Reactivite = GID("slider" + iAct).value;
+ if (GID("selectPin"+iAct).value>=0) LesActions[iAct].OrdreOn=GID("selectPin"+iAct).value +IS + GID("selectOut"+iAct).value;
+ if (GID("selectPin"+iAct).value==0 && iAct>0) LesActions[iAct].Actif=-1; //Action Ă effacer
+ }
+ var S="";
+ for (var iAct = 0; iAct < LesActions.length; iAct++) {
+ if (LesActions[iAct].Actif>=0){
+ S +=LesActions[iAct].Actif+RS+LesActions[iAct].Titre+RS;
+ S +=LesActions[iAct].Host+RS+LesActions[iAct].Port+RS;
+ S +=LesActions[iAct].OrdreOn+RS+LesActions[iAct].OrdreOff+RS+LesActions[iAct].Repet+RS+LesActions[iAct].Tempo+RS;
+ S +=LesActions[iAct].Reactivite + RS + LesActions[iAct].Periodes.length+RS;
+ for (var i=0;i/commande?idx=23&position=on. Se référer à la documentation constructeur."
+ break;
+ case "sele":
+ var m = "SĂ©lection du GPIO.
";
+ m += "Choix de l'Ă©tat de sortie haut ou bas quand l'action est 'On'"
+ break;
+ case "repe":
+ var m = "Période en s de répétition/rafraîchissement de la commande. Uniquement pour les commandes vers l'extĂ©rieur.
";
+ m += "0= pas de répétition.";
+ break;
+ case "temp":
+ var m = "Temporisation entre chaque changement d'Ă©tat pour Ă©viter des oscillations quand un appareil dans la maison consomme en dent de scie (Ex: un four)."
+ break;
+ case "adds":
+ var m = "Ajout ou retrait d'une période horaire."
+ break;
+ case "Pw":
+ var m = "Seuil inférieur de puissance mesurée Pw < pour démarrer le routage et seuil supérieur de puissance > pour l'arrêter.
";
+ m +="Attention, la différence, seuil supérieur moins seuil inférieur doit être supérieure à la consommation du dipositif pour éviter l'oscillation du relais de commande."
+ break;
+ case "pwTr":
+ var m = "Seuil en W de régulation par le Triac de la puissance mesurée Pw en entrĂ©e de la maison. Valeur typique : 0.";
+ break;
+ case "mxTr":
+ var m = "Ouverture maximum du triac entre 5 et 100%. Valeur typique : 100%";
+ break;
+ case "zNo":
+ var m = "Pas d'action On ou Off de routage";
+ break;
+ case "zOff":
+ var m = "Off forcé";
+ break;
+ case "zOn":
+ var m = "On forcé (si règle température valide)";
+ break;
+ case "tmpr":
+ var m = "Définir la ou les températures qui permettent l'activation de la fonction On ou Routage.
Sinon ordre Off envoyé ou Triac se ferme.
Ne rien mettre si pas d'activation en fonction de la température.";
+ break;
+ case "tarif":
+ var m = "Condition d'activation suivant la tarification.
Sinon ordre Off envoyé ou Triac se ferme.";
+ break;
+ }
+ GH("message", m);
+ GID("message").style = "display:inline-block;";
+ Timer = 10;
+ }
+ var Timer = 0;
+ function DispTimer() {
+ Timer = Timer - 1;
+ if (Timer < 0) {
+ GID('message').style = 'display:none;';
+ }
+ setTimeout("DispTimer();", 1000);
+ }
+ function AdaptationSource(){
+
+ }
+)====";
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlBrute.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlBrute.h
new file mode 100644
index 0000000..32344ba
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlBrute.h
@@ -0,0 +1,398 @@
+//************************************************
+// Page données RMS Brutes HTML et Javascript
+//************************************************
+const char *PageBrute = R"====(
+
+
+
+
+
+ Routeur Solaire - RMS
+ Date
+
+
+
_ V
+ _ A
+ Facteur de puissance :
+
+
+
+
+
+
+
+
+ Données distantes
+
+
+
+
+
+
+)====";
+
+const char *PageBruteJS = R"====(
+ var InitFait=false;
+ var IdxMessage=0;
+ var MessageLinky='';
+
+ const M=[]; //Pour UxIx2
+ M.push(['Tension_M','Tension efficace','V','V']);
+ M.push(['Intensite_M','Courant efficace','A','A']);
+ M.push(['PuissanceS_M','Puissance (Pw)','W','W']);
+ M.push(['PowerFactor_M','Facteur de puissance','','phi']);
+ M.push(['Energie_M_Soutiree','Energie active soutirée','Wh','Wh']);
+ M.push(['Energie_M_Injectee','Energie active injectée','Wh','Wh']);
+ M.push(['Tension_T','Tension efficace','V','V']);
+ M.push(['Intensite_T','Courant efficace','A','A']);
+ M.push(['PuissanceS_T','Puissance (Pw)','W','W']);
+ M.push(['PowerFactor_T','Facteur de puissance','','phi']);
+ M.push(['Energie_T_Soutiree','Energie active consommée','Wh','Wh']);
+ M.push(['Energie_T_Injectee','Energie active produite','Wh','Wh']);
+ M.push(['Frequence','Fréquence','Hz','Hz']);
+ const E=[]; //Pour Enphase
+ E.push(['Tension_M','Tension efficace','V','V']);
+ E.push(['Intensite_M','Courant efficace','A','A']);
+ E.push(['PuissanceS_M','Puissance réseau public (Pw)','W','W']);
+ E.push(['PowerFactor_M','Facteur de puissance','','phi']);
+ E.push(['Energie_M_Soutiree','Energie active soutirée','Wh','Wh']);
+ E.push(['Energie_M_Injectee','Energie active injectée','Wh','Wh']);
+ E.push(['PactProd','Puissance produite (Pw)','W','W']);
+ E.push(['PactConso_M','Puissance consommée (Pw)','W','W']);
+ E.push(['SessionId','Session Id','','Enph']);
+ E.push(['Token_Enphase','Token','','Enph']);
+
+ const L=[];
+ L.push(['EAST','Energie active soutirée',false,'Wh',0]);
+ L.push(['EASF01','Energie active soutirée Fournisseur,
index 01',true,'Wh',0]);
+ L.push(['EASF02','Energie active soutirée Fournisseur,
index 02',true,'Wh',0]);
+ L.push(['EASF03','Energie active soutirée Fournisseur,
index 03',true,'Wh',0]);
+ L.push(['EASF04','Energie active soutirée Fournisseur,
index 04',true,'Wh',0]);
+ L.push(['EASF05','Energie active soutirée Fournisseur,
index 05',true,'Wh',0]);
+ L.push(['EASF06','Energie active soutirée Fournisseur,
index 06',true,'Wh',0]);
+ L.push(['EASF07','Energie active soutirée Fournisseur,
index 07',true,'Wh',0]);
+ L.push(['EASF08','Energie active soutirée Fournisseur,
index 08',true,'Wh',0]);
+ L.push(['EASF09','Energie active soutirée Fournisseur,
index 09',true,'Wh',0]);
+ L.push(['EASF10','Energie active soutirée Fournisseur,
index 10',true,'Wh',0]);
+ L.push(['EAIT','Energie active injectée',false,'Wh',0]);
+ L.push(['IRMS1','Courant efficace, phase 1',true,'A',0]);
+ L.push(['IRMS2','Courant efficace, phase 2',true,'A',0]);
+ L.push(['IRMS3','Courant efficace, phase 3',true,'A',0]);
+ L.push(['URMS1','Tension efficace, phase 1',true,'V',0]);
+ L.push(['URMS2','Tension efficace, phase 2',true,'V',0]);
+ L.push(['URMS3','Tension efficace, phase 3',true,'V',0]);
+ L.push(['SINSTS','Puissance app. Instantanée soutirée',false,'VA',0]);
+ L.push(['SINSTS1','Puissance app. Instantanée soutirée phase 1',true,'VA',0]);
+ L.push(['SINSTS2','Puissance app. Instantanée soutirée phase 2',true,'VA',0]);
+ L.push(['SINSTS3','Puissance app. Instantanée soutirée phase 3',true,'VA',0]);
+ L.push(['SMAXSN','Puissance app. max. soutirée n',false,'VA',1]);
+ L.push(['SMAXSN1','Puissance app. max. soutirée n phase 1',true,'VA',1]);
+ L.push(['SMAXSN2','Puissance app. max. soutirée n phase 2',true,'VA',1]);
+ L.push(['SMAXSN3','Puissance app. max. soutirée n phase 3',true,'VA',1]);
+ L.push(['SMAXSN-1','Puissance app. max. soutirée n-1',false,'VA',1]);
+ L.push(['SMAXSN1-1','Puissance app. max. soutirée n-1 phase 1',true,'VA',1]);
+ L.push(['SMAXSN2-1','Puissance app. max. soutirée n-1 phase 2',true,'VA',1]);
+ L.push(['SMAXSN3-1','Puissance app. max. soutirée n-1 phase 3',true,'VA',1]);
+ L.push(['SINSTI','Puissance app. Instantanée injectée',false,'VA',0]);
+ L.push(['SMAXIN','Puissance app. max injectée n',false,'VA',1]);
+ L.push(['SMAXIN-1','Puissance app. max injectée n-1',false,'VA',1]);
+ L.push(['LTARF','Option Tarifaire',false,'',2]);
+
+ function creerTableauUxIx2(){
+ var S='';
+ for (var i=0;iMaison | | | ';
+ }
+ if (i==6){
+ S+='Triac | | |
';
+ }
+ S+=''+M[i][1]+' | | '+M[i][2]+' |
';
+ }
+ S+='
';
+ GH('tableau', S);
+ }
+ function creerTableauEnphase(){
+ var S='';
+ for (var i=0;iMaison | | | ';
+ }
+ S+=''+E[i][1]+' | | '+E[i][2]+' |
';
+ }
+ S+='
';
+ GH('tableauEnphase', S);
+ }
+ function creerTableauLinky(){
+ var S='';
+ for (var i=0;i'+L[i][1]+' | | '+L[i][3]+' | | ';
+ }
+ S+='
';
+ GH('tableauLinky', S);
+ }
+ function LoadData() {
+ GID('LED').style='display:block;';
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ GID('LED').style='display:none;';
+ var DuRMS=this.responseText;
+ var groupes=DuRMS.split(GS)
+ var G0=groupes[0].split(RS);
+ GH('date',G0[0]);
+ Source_data=G0[1];
+ if (Source_data == "UxI"){
+ GID('infoUxI').style.display="block";
+ GH('Ueff',parseInt(G0[2],10));
+ GH('Ieff',G0[3]);
+ GH('cosphi',G0[4]);
+ var volt=groupes[1].split(RS);
+ var amp=groupes[2].split(RS);
+ var S= "";
+ GH('SVG',S);
+ }
+ if (Source_data == "UxIx2"){
+ GID('infoUxIx2').style.display="block";
+ var G1=groupes[1].split(RS);
+ if(!InitFait){
+ InitFait=true;
+ creerTableauUxIx2();
+ GH("nomSondeFixe",nomSondeFixe);
+ GH("nomSondeMobile",nomSondeMobile);
+ }
+ for (var j=0;j";
+ }
+ GH('dataSmartG', S);
+ }
+ if (Source_data == "UxIx3"){
+ GID('infoUxIx3').style.display="block";
+ GH('dataUxIx3', groupes[1]);
+ }
+ if (Source_data == "ShellyEm"){
+ GID('infoShellyEm').style.display="block";
+ groupes[1] = groupes[1].replaceAll('"','');
+ var G1=groupes[1].split(",");
+ var S="";
+ for (var i=0;i";
+ }
+ GH('dataShellyEm', S);
+ }
+ if (Source_data == "Pmqtt"){
+ GID('infoPmqtt').style.display="block";
+ GH('dataPmqtt', groupes[1]);
+ }
+ if (Source_data == "Linky"){
+ GID('infoLinky').style.display="block";
+ if(!InitFait){
+ InitFait=true;
+ creerTableauLinky();
+ }
+ MessageLinky +=groupes[1];
+ var blocs=MessageLinky.split(String.fromCharCode(2));
+ var lg=blocs.length;
+ if (lg>2){
+ MessageLinky=String.fromCharCode(2)+blocs[lg-1];
+ GH('DataLinky', ''+blocs[lg-2]+'
');
+ var lignes=blocs[lg-2].split(String.fromCharCode(10));
+ for (var i=0;i0){
+ GID('L'+L[j][0]).style.display="table-row";
+ switch (L[j][4]){
+ case 0:
+ GH(L[j][0], LaVal(colonnes[1]));
+ break;
+ case 1:
+ GH('h'+L[j][0], LaDate(colonnes[1]));
+ GH(L[j][0], LaVal(colonnes[2]));
+ break;
+ case 2: //Texte
+ GH('h'+L[j][0], colonnes[1]);
+ break;
+ }
+ }
+ }
+ }
+ }
+ GID('LED').style='display:none;';
+ }
+ IdxMessage=groupes[2];
+ }
+
+ setTimeout('LoadData();',2000);
+ }
+ };
+ xhttp.open('GET', 'ajax_dataRMS?idx='+IdxMessage, true);
+ xhttp.send();
+ }
+ function LoadDataESP32() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var dataESP=this.responseText;
+ var message=dataESP.split(RS);
+ var S='';
+ var H=parseInt(message[0]);
+ H=H + (message[0]-H)*0.6;
+ H=H.toFixed(2);
+ H=H.replace(".", "h ")+"mn";
+ var LaSource=Source;
+ if (LaSource=='Ext') LaSource="Externe ("+Source_data+")
" +int2ip(RMSextIP);
+ S+='ESP On depuis : | '+H+' |
';
+ S+='Source des mesures : | '+LaSource+' |
';
+ S+='Niveau WiFi : | '+message[1]+' dBm |
';
+ S+="Point d'accès WiFi : | "+message[2]+' |
';
+ S+='Adresse MAC ESP32 : | '+message[3]+' |
';
+ S+='Réseau WiFi : | '+message[4]+' |
';
+ S+='Adresse IP ESP32 : | '+message[5]+' |
';
+ S+='Adresse passerelle : | '+message[6]+' |
';
+ S+='Masque du réseau : | '+message[7]+' |
';
+ S+='Charge coeur 0 (Lecture Puissance) Min, Moy, Max : | '+message[8]+' ms |
';
+ S+='Charge coeur 1 (Calcul + Wifi) Min, Moy, Max : | '+message[9]+' ms |
';
+ S+='Espace mémoire EEPROM utilisé : | '+message[10]+' % |
';
+ S+="Nombre d'interruptions en 15ms du Gradateur (signal Zc) : Filtrés/Brutes : | "+message[11]+' |
';
+ S+='Synchronisation 10ms au Secteur ou asynchrone horloge ESP32 | '+message[12]+' |
';
+ S +='Messages | |
';
+ for (var i=0;i<10;i++){
+ S +=''+message[13+i]+' | |
';
+ }
+ S+='
';
+ GH('DataESP32', S);
+ setTimeout('LoadDataESP32();',5000);
+ }
+
+ };
+ xhttp.open('GET', 'ajax_dataESP32', true);
+ xhttp.send();
+ }
+ function LaDate(d){
+ return d.substr(0,1)+' '+d.substr(5,2)+'/'+d.substr(3,2)+'/'+d.substr(1,2)+' '+d.substr(7,2)+'h '+d.substr(9,2)+'mn '+d.substr(11,2)+'s';
+ }
+ function LaVal(d){
+ d=parseInt(d);
+ d=' '+d.toString();
+ return d.substr(-9,3)+' '+d.substr(-6,3)+' '+d.substr(-3,3);
+ }
+ function AdaptationSource(){
+ if(Source=="Ext"){
+ GID("donneeDistante").style.display="block";
+ }
+ LoadData();LoadDataESP32();
+ }
+)====";
+
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h
new file mode 100644
index 0000000..c4e0235
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h
@@ -0,0 +1,106 @@
+// **********************************************************
+// Page de connexion 'Acces Point' pour définir réseau WIFI
+// **********************************************************
+const char *ConnectAP_Html = R"====(
+
+
+
+
+
+
+
+Routeur Solaire - RMS
Connexion au réseau WIFI local
+
+
+
+
+
+
+
+
+
+
+Attendez l'adresse IP attribuée à l'ESP 32
+
+
+
+)====";
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h
new file mode 100644
index 0000000..5e21a1d
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h
@@ -0,0 +1,553 @@
+//************************************************
+// Page principale HTML et Javascript
+//************************************************
+const char *MainHtml = R"====(
+
+
+
+
+
+ Routeur Solaire - RMS
+ DATE
+
+ | Maison | Fixe | |
+ | Soutirée | Injectée | Conso. | Produite | |
+ Puissance Active (Pw) | | | | | W |
+ Puissance Apparente | | | | | VA |
+ Energie Active du jour | | | | | Wh |
+ Energie Active Totale | | | | | Wh |
+
+ Données distantes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)====";
+
+const char *MainJS = R"====(
+ var tabPW2sM=[];
+ var tabPW2sT=[];
+ var initUxIx2=false;
+ var biSonde=false;
+ var TabVal = [];
+ var TabCoul= [];
+ var myTimeout;
+ var myActionTimeout;
+ var ActionForce =[];
+ var Pva_valide =false;
+ function LoadData() {
+ GID('LED').style='display:block;';
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var DuRMS=this.responseText;
+ var groupes=DuRMS.split(GS);
+ var G0=groupes[0].split(RS);
+ var G1=groupes[1].split(RS);
+ var G2=groupes[2].split(RS);
+ GID('date').innerHTML = G0[1];
+ Source_data= G0[2];
+ if (!initUxIx2){
+ initUxIx2=true;
+ var d='none';
+ if(groupes.length==4){ // Cas pour les sources externes UxIx2 et Shelly monophasé
+ d="table-cell";
+ }
+ const collection = document.getElementsByClassName('dispT');
+ for (let i = 0; i < collection.length; i++) {
+ collection[i].style.display = d;
+ }
+ }
+ GID('PwS_M').innerHTML = LaVal(G1[0]); //Maison
+ GID('PwI_M').innerHTML = LaVal(G1[1]); //Maison
+ GID('PVAS_M').innerHTML = LaVal(G1[2]); //Maison
+ GID('PVAI_M').innerHTML = LaVal(G1[3]); //Maison
+ GID('EAJS_M').innerHTML = LaVal(G1[4]);
+ GID('EAJI_M').innerHTML = LaVal(G1[5]);
+ GID('EAS_M').innerHTML = LaVal(G1[6]);
+ GID('EAI_M').innerHTML = LaVal(G1[7]);
+ tabPW2sM.shift(); //Enleve Pw Maison
+ tabPW2sM.shift(); //Enleve PVA
+ tabPW2sM.push(parseFloat(G1[0]-G1[1]));
+ tabPW2sM.push(parseFloat(G1[2]-G1[3]));
+ Plot('SVG_PW2sM',tabPW2sM,'#f44','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA');
+
+ var Tarif=["NON_DEFINI","PLEINE","CREUSE","BLEU","BLANC","ROUGE"];
+ var couleur=["#ddf","#f00","#0f0","#00bfff","#fff","#f00"];
+ var tarif=["","H.
pleine","H.
creuse","Tempo
Bleu","Tempo
Blanc","Tempo
Rouge"];
+ var idx=0;
+ for (i=0;i<6;i++){
+ if ( G0[3].indexOf(Tarif[i])>=0){ //LTARF dans Link
+ idx=i;
+ }
+ }
+ GID('couleurTarif_jour').style.backgroundColor= couleur[idx];
+ GID('couleurTarif_jour').innerHTML =tarif[idx];
+ var tempo = parseInt(G0[4], 16); //Tempo lendemain et jour STGE
+ tempo =Math.floor(tempo/4) ; //Tempo lendemain uniquement
+ idx=-2;
+ var txtJ = "";
+ if (tempo>0){
+ idx = tempo;
+ txtJ = "Tempo
J+1";
+ }
+ GID('couleurTarif_J1').style.backgroundColor= couleur[idx+2];
+ GID('couleurTarif_J1').innerHTML =txtJ;
+ Pva_valide = (G0[6] == 1 ) ? true:false;
+
+ if (groupes.length==4) { // La source_data des données est de type UxIx2 ou on est en shelly monophas avec un deuxièeme canal
+ GID('PwS_T').innerHTML = LaVal(G2[0]); //Triac
+ GID('PwI_T').innerHTML = LaVal(G2[1]); //Triac
+ GID('PVAS_T').innerHTML = LaVal(G2[2]); //Triac
+ GID('PVAI_T').innerHTML = LaVal(G2[3]); //Triac
+ GID('EAJS_T').innerHTML = LaVal(G2[4]);
+ GID('EAJI_T').innerHTML = LaVal(G2[5]);
+ GID('EAS_T').innerHTML = LaVal(G2[6]);
+ GID('EAI_T').innerHTML = LaVal(G2[7]);
+ tabPW2sT.shift(); //Enleve Pw Triav
+ tabPW2sT.shift(); //Enleve PVA
+ tabPW2sT.push(parseFloat(G2[0]-G2[1]));
+ tabPW2sT.push(parseFloat(G2[2]-G2[3]));
+ Plot('SVG_PW2sT',tabPW2sT,'#f44','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA');
+ if (parseInt(G2[5])==0 && Source!="ShellyEm") { //Il n'y a pas d'injecté normalement
+ GID('produite').innerHTML='';
+ GID('PwI_T').innerHTML='';
+ GID('PVAI_T').innerHTML='';
+ GID('EAJI_T').innerHTML='';
+ GID('EAI_T').innerHTML='';
+ }
+ biSonde=true;
+ } else{
+ biSonde=false;
+ }
+ if (!Pva_valide) { GID('ligneVA').style='display:none;';}
+ GID('LED').style='display:none;';
+ setTimeout('LoadData();',2000);
+ }
+
+ };
+ xhttp.open('GET', 'ajax_data', true);
+ xhttp.send();
+ }
+
+ function LoadHisto10mn() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var retour=this.responseText;
+ var groupes=retour.split(GS);
+ tabPW2sM.splice(0,tabPW2sM.length);
+ tabPW2sM=groupes[1].split(',');
+ tabPW2sM.pop();
+ Plot('SVG_PW2sM',tabPW2sM,'#f44','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA');
+ if (biSonde){
+ tabPW2sT.splice(0,tabPW2sT.length);
+ tabPW2sT=groupes[2].split(',');
+ tabPW2sT.pop();
+ GID('SVG_PW2sT').style.display="block";
+ Plot('SVG_PW2sT',tabPW2sT,'#f44','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA');
+ }
+ LoadHisto1an();
+ }
+
+ };
+ xhttp.open('GET', 'ajax_data10mn', true);
+ xhttp.send();
+ }
+ function LoadHisto48h() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var retour=this.responseText;
+ var groupes=retour.split(GS);
+ var tabPWM=groupes[1].split(',');
+ tabPWM.pop();
+ Plot('SVG_PW48hM',tabPWM,'#f33','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 48h en W','','');
+ if (biSonde){
+ var tabPWT=groupes[2].split(',');
+ tabPWT.pop();
+ GID('SVG_PW48hT').style.display="block";
+ Plot('SVG_PW48hT',tabPWT,'#f33','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 48h en W','','');
+ }
+ groupes.shift();groupes.shift();groupes.shift();
+ if (parseFloat(groupes[0])> -100) {
+ var tabTemperature=groupes[1].split(',');
+ tabTemperature.pop();
+ GID('SVG_Temp48h').style.display="block";
+ Plot('SVG_Temp48h',tabTemperature,'#3f3',nomTemperature+' sur 48h ','','');
+ }
+ groupes.shift();
+ groupes.shift();
+ if (groupes.length>0) {
+ Plot_ouvertures(groupes);
+ }
+ setTimeout('LoadHisto48h();',300000);
+ }
+
+ };
+ xhttp.open('GET', 'ajax_histo48h', true);
+ xhttp.send();
+ }
+ function LoadHisto1an() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var retour=this.responseText;
+ var tabWh=retour.split(',');
+ tabWh.pop();
+
+ Plot('SVG_Wh1an',tabWh,'#ff4','Energie Active Wh / Jour sur 1an','','');
+ LoadHisto48h();
+ }
+
+ };
+ xhttp.open('GET', 'ajax_histo1an', true);
+ xhttp.send();
+ }
+ function Plot(SVG,Tab,couleur1,titre1,couleur2,titre2){
+ var Vmax=0;
+ var Vmin=0;
+ var TabY0=[];
+ var TabY1=[];
+ for (var i = 0; i < Tab.length; i++) {
+ Tab[i]=Math.min(Tab[i],10000000);
+ Tab[i]=Math.max(Tab[i],-10000000);
+ Vmax = Math.max(Math.abs(Tab[i]), Vmax);
+ }
+ var cadrageMax=1;
+ var cadrage1=1000000;
+ var cadrage2=[10,8,5,4,2,1];
+ for (var m=0;m<7;m++){
+ for (var i=0;i"; //
+ S += "";
+ S += "";
+
+ for (var x=1000+X0;x>100;x=x-pixelTic){
+ var X=x;
+ var Y2=Y0+6;
+ S +="";
+ X=X-8;
+ Y2=Y0+22;
+ if (SVG=='SVG_Wh1an') {
+ X=X+8;
+ S +=""+Mois[H00]+"";
+ }else{
+ S +=""+H00+"";
+ }
+ H00=(H00-dTextTic+moduloText)%moduloText;
+ }
+ Y2=Y0-3;
+ S +=""+label+"";
+ for (var y=-10 ;y<=10;y=y+dy){
+
+ Y2=Y0-Yamp*y/10;
+ if (Y2<=480){
+ S +="";
+ Y2=Y2+7;
+ var T=cadrageMax*y/10;T=T.toString();
+ var X=90-9*T.length;
+ S +=""+T+"";
+ }
+ }
+ if (dI==2 && Pva_valide){ //Puissance apparente
+ S +=""+titre2+"";
+ S += "";
+ }
+ S +=""+titre1+"";
+ S += "";
+ S += "";
+ GID(SVG).innerHTML = S;
+ TabVal["S_" + SVG]=[TabY0,TabY1];; //Sauvegarde valeurs
+ TabCoul["S_" + SVG]=[couleur1, couleur2];
+
+ }
+
+ function DispVal(t,evt){
+ var ClientRect = t.getBoundingClientRect();
+ var largeur_svg=ClientRect.right-ClientRect.left-20; //20 pixels de marge
+ var x= Math.round(evt.clientX - ClientRect.left-10);
+ x=x*1030/largeur_svg;
+ var p=Math.floor((x-100)*TabVal[t.id][0].length/900);
+ if (p>=0 && p0) {
+ S +="" + TabVal[t.id][j][p] + "
";
+ }
+ }
+ x = evt.pageX;
+ GID("info").style.left=x + "px";
+ x = ClientRect.top+10 +window.scrollY;
+ GID("info").style.top=x +"px";
+ x=evt.pageY- x;
+ GID("info_txt").style.top=x +"px";
+ x = ClientRect.height-20;
+ GID("info").style.height=x +"px";
+ GH("info_txt",S);
+ GID("info").style.display="block";
+ if (myTimeout !=null) clearTimeout(myTimeout);
+ myTimeout=setTimeout(stopAffiche, 5000);
+ }
+
+ }
+ function stopAffiche(){
+ GID("info").style.display="none";
+ }
+ function Plot_ouvertures(Gr){
+ GID("SVG_Ouvertures").style.display="block";
+ const d = new Date();
+ var label='heure';
+ var pixelTic=72;
+ var dTextTic=4;
+ var moduloText=24;
+ var H0=d.getHours()+d.getMinutes()/60;
+ var H00= 4*Math.floor(H0/4);
+ var X0=18*(H00-H0);
+ var Hmax=50+150*Gr.length;
+ var Y0=Hmax-50;
+ var Couls=["#f83","#3fa","#68f"];
+ var LesVals =[];
+ var LesCouls =[];
+ var S= "";
+ TabVal["S_Ouvertures"] = LesVals;
+ TabCoul["S_Ouvertures"] = LesCouls;
+ GID("SVG_Ouvertures").innerHTML = S;
+
+ }
+ function EtatActions(Force,NumAction) {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var retour=this.responseText;
+ var message=retour.split(GS);
+
+ Source_data=message[1];
+ var T="";
+ if(message[0]>-100){
+ var Temper=parseFloat(message[0]).toFixed(1);
+ T="" + nomTemperature +" | "+Temper+"°C | |
";
+ }
+ var S="";
+ if (message[3]>0){ //Nb Actions
+ ActionForce.splice(0, ActionForce.length);
+ for (var i=0;i"+data[1]+" | ";
+ if (data[2]=="On" || data[2]=="Off"){
+ S+=" | ";
+ } else {
+ var W=1+1.99*data[2];
+ S+=" | ";
+ }
+ var stOn=(ActionForce[i]>0) ? "style='background-color:#f66;'":"";
+ var stOff=(ActionForce[i]<0) ? "style='background-color:#f66;'":"";
+ var min=(ActionForce[i]==0) ? " ":Math.abs(ActionForce[i]) +" min";
+ S +=" | "+min+" | | ";
+ }
+ }
+ S=S+T;
+ if (S!=""){
+ S="Etat Action(s) | Forçage |
" +S + "
";
+ GH("etatActions",S);
+ }
+ myActionTimeout=setTimeout('EtatActions(0,0);',3500);
+ }
+
+ };
+ xhttp.open('GET', 'ajax_etatActions?Force=' +Force + '&NumAction=' + NumAction, true);
+ xhttp.send();
+ }
+
+ function LaVal(d){
+ d=parseInt(d);
+ d=' '+d.toString();
+ return d.substr(-9,3)+' '+d.substr(-6,3)+' '+d.substr(-3,3);
+ }
+ function Force(NumAction,Force){
+ if (myActionTimeout !=null) clearTimeout(myActionTimeout);
+ EtatActions(Force,NumAction);
+ }
+
+ function AdaptationSource(){
+ var d='none';
+ if(biSonde){
+ d="table-cell";
+ }
+ const collection = document.getElementsByClassName('dispT');
+ for (let i = 0; i < collection.length; i++) {
+ collection[i].style.display = d;
+ }
+
+ var S='Source : '
+ if(Source=="Ext"){
+ S +='ESP distant '+int2ip(RMSextIP);
+ GID("donneeDistante").style.display="block";
+ }else {
+ S +='ESP local';
+ }
+ GH('source',S);
+ }
+)====";
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h
new file mode 100644
index 0000000..670dc66
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h
@@ -0,0 +1,95 @@
+//***************************************************
+// Page HTML et Javascript mise Ă jour du code par OTA
+//***************************************************
+const char *OtaHtml = R"====(
+
+
+
+
+
+
+
+
+ Web OTA
+ Mise Ă jour par Wifi
+
+ Votre version actuelle du routeur :
+
+
+
+ Version(s) disponible(s) :
+
+
+
+
+
+
+ - 1 - Téléchargez sur votre ordinateur, la version binaire du logiciel du routeur souhaitée (Solar_Router_Vxx.xx.ino.bin)
+ - 2 - Cliquez sur "Choisir un fichier" et sélectionnez ce binaire sur votre ordinateur
+ - 3 - Cliquez sur "Mettre Ă jour"
+
+
+
+ progression: 0%
+
+
+
+
+
+
+ )====";
\ No newline at end of file
diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h
new file mode 100644
index 0000000..310e959
--- /dev/null
+++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h
@@ -0,0 +1,449 @@
+//***************************************************
+// Page HTML et Javascript de gestion des Paramètres
+//***************************************************
+const char *ParaHtml = R"====(
+
+
+
+
+
+
+
+ Routeur Solaire - RMS
Paramètres
+
+
Source des mesures de puissance
+
+
+
+
+
Source des mesures de température
+
+
+
+
+
+
Adresse IP de l'ESP32 du Routeur
+
+
+
+
+
Paramètres serveur MQTT (Home Assistant , Domoticz ...)
+
+
+
+
Calibration Mesures Ueff et Ieff
+
+
+
+
+
+
+
+)====";
+const char *ParaJS = R"====(
+ var LaTemperature = -100;
+ function Init(){
+ LoadParametres();
+ LoadParaRouteur();
+ }
+ function LoadParametres() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var LesParas=this.responseText;
+ var Para=LesParas.split(RS);
+ GID("dhcp").checked = Para[0]==1 ? true:false;
+ GID("adrIP").value=int2ip(Para[1]);
+ GID("gateway").value=int2ip(Para[2]);
+ GID("masque").value=int2ip(Para[3]);
+ GID("dns").value=int2ip(Para[4]);
+ GID(Para[5]).checked = true;
+ GID("RMSextIP").value=int2ip(Para[6]);
+ GID("EnphaseUser").value=Para[7];
+ GID("EnphasePwd").value=Para[8];
+ GID("EnphaseSerial").value=Para[9];
+ GID("TopicP").value=Para[10];
+ GID("MQTTRepete").value = Para[11];
+ GID("MQTTIP").value=int2ip(Para[12]);
+ GID("MQTTPort").value=Para[13];
+ GID("MQTTUser").value=Para[14];
+ GID("MQTTpwd").value=Para[15];
+ GID("MQTTPrefix").value=Para[16];
+ GID("MQTTdeviceName").value=Para[17];
+ GID("subMQTT").checked = Para[18]==1 ? true:false;
+ GID("nomRouteur").value=Para[19];
+ GID("nomSondeFixe").value=Para[20];
+ GID("nomSondeMobile").value=Para[21];
+ LaTemperature=parseInt(Para[22]);
+ GID("nomTemperature").value=Para[23];
+ GID(Para[24]).checked = true;
+ GID("TopicT").value=Para[25];
+ GID("IPtemp").value=int2ip(Para[26]);
+ GID("CalibU").value=Para[27];
+ GID("CalibI").value=Para[28];
+ GID("TempoEDFon").checked = Para[29]==1 ? true:false;
+ GID("WifiSleep").checked = Para[30]==1 ? true:false;
+ GID("Serial" + Para[31]).checked = true;
+ GID("Triac" + Para[32]).checked = true;
+
+ checkDisabled();
+ }
+ };
+ xhttp.open('GET', 'ParaAjax', true);
+ xhttp.send();
+ }
+ function SendValues(){
+ GID("attente").style="visibility: visible;";
+ var dhcp = GID("dhcp").checked ? 1:0;
+ var TempoEDFon = GID("TempoEDFon").checked ? 1:0;
+ var Source_new = document.querySelector('input[name="sources"]:checked').value;
+ var Source_Temp = document.querySelector('input[name="srcTemp"]:checked').value;
+ var subMQTT = GID("subMQTT").checked ? 1:0;
+ var WifiSleep = GID("WifiSleep").checked ? 1:0;
+ var Serial = document.querySelector('input[name="pSerie"]:checked').value;
+ var pTriac = document.querySelector('input[name="pTriac"]:checked').value;
+ var S=dhcp+RS+ ip2int(GID("adrIP").value)+RS+ ip2int(GID("gateway").value);
+ S +=RS+ip2int(GID("masque").value)+RS+ ip2int(GID("dns").value)
+ S +=RS+Source_new+RS+ ip2int(GID("RMSextIP").value)+ RS+GID("EnphaseUser").value.trim()+RS+GID("EnphasePwd").value.trim()+RS+GID("EnphaseSerial").value.trim() +RS+GID("TopicP").value.trim();
+ S +=RS+GID("MQTTRepete").value +RS+ip2int(GID("MQTTIP").value) +RS+GID("MQTTPort").value +RS+GID("MQTTUser").value.trim()+RS+GID("MQTTpwd").value.trim();
+ S +=RS+GID("MQTTPrefix").value.trim()+RS+GID("MQTTdeviceName").value.trim() + RS + subMQTT;
+ S +=RS+GID("nomRouteur").value.trim()+RS+GID("nomSondeFixe").value.trim()+RS+GID("nomSondeMobile").value.trim();
+ S +=RS+GID("nomTemperature").value.trim() +RS+Source_Temp +RS+GID("TopicT").value.trim() + RS + ip2int(GID("IPtemp").value);
+ S +=RS+GID("CalibU").value+RS+GID("CalibI").value + RS + TempoEDFon + RS + WifiSleep + RS + Serial + RS + pTriac;
+ S="?lesparas="+clean(S);
+ if ((GID("dhcp").checked || checkIP("adrIP")&&checkIP("gateway")) && (!GID("MQTTRepete").checked || checkIP("MQTTIP"))){
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var retour=this.responseText;
+ location.reload();
+ }
+ };
+ xhttp.open('GET', 'ParaUpdate'+S, true);
+ xhttp.send();
+ }
+ }
+ function checkDisabled(){
+ GID("infoIP").style.display = GID("dhcp").checked ? "none" : "table";
+ GID("Zmqtt").style.display = (GID("MQTTRepete").value != 0 || GID("Pmqtt").checked || GID("tempMqtt").checked || GID("subMQTT").checked) ? "table" : "none";
+ GID('ligneTemperature').style.display = (GID("tempNo").checked) ? "none" : "table";
+ GID('ligneTopicT').style.display = (GID("tempMqtt").checked) ? "table-row" : "none";
+ GID('ligneIPtemp').style.display = (GID("tempExt").checked) ? "table-row" : "none";
+ GID('ligneTopicP').style.display = (GID("Pmqtt").checked) ? "table-row" : "none";
+ Source = document.querySelector('input[name="sources"]:checked').value;
+ if (Source !='Ext') Source_data=Source;
+ AdaptationSource();
+ }
+ function checkIP(id){
+ var S=GID(id).value;
+ var Table=S.split(".");
+ var valide=true;
+ if (Table.length!=4) {
+ valide=false;
+ }else{
+ for (var i=0;i255 || Table[i]<0) valide=false;
+ }
+ }
+ if (valide){
+ GID(id).style.color="black";
+ } else {
+ GID(id).style.color="red";
+ }
+ return valide;
+ }
+
+
+ function Reset(){
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ GID('BoutonsBas').innerHTML=this.responseText;
+ setTimeout(location.reload(),3000);
+ }
+ };
+ xhttp.open('GET', 'restart', true);
+ xhttp.send();
+ }
+ function AdaptationSource(){
+ GID('ligneFixe').style.display = (Source_data=='UxIx2' || (Source_data=='ShellyEm' && GID("EnphaseSerial").value <3))? "table-row" : "none";
+ GID('Zcalib').style.display=(Source_data=='UxI' && Source=='UxI' ) ? "table" : "none";
+ var txtExt = "ESP-RMS";
+ if (Source=='Enphase') txtExt = "Enphase-Envoy";
+ if (Source=='SmartG') txtExt = "SmartGateways";
+ var lab_enphaseShelly= "Numéro série passerelle IQ Enphase :
Pour firmvare Envoy-S V7 seulement";
+ if (Source=='ShellyEm') {
+ txtExt = "Shelly Em ";
+ lab_enphaseShelly="Monophasé : Numéro de voie (0 ou 1) mesurant l'entrée du courant maison
Triphasé : mettre 3";
+ }
+ GID('labExtIp').innerHTML = txtExt;
+ GID('label_enphase_shelly').innerHTML = lab_enphaseShelly;
+ GID('ligneExt').style.display = (Source=='Ext' || Source=='Enphase' || Source=='SmartG' || Source=='ShellyEm') ? "table-row" : "none";
+ GID('ligneEnphaseUser').style.display = (Source=='Enphase') ? "table-row" : "none";
+ GID('ligneEnphasePwd').style.display = (Source=='Enphase') ? "table-row" : "none";
+ GID('ligneEnphaseSerial').style.display = (Source=='Enphase' || Source=='ShellyEm') ? "table-row" : "none"; //Numéro de serie ou voie
+ }
+)====";
+
+//Paramètres du routeur et fonctions générales pour toutes les pages.
+const char *ParaRouteurJS = R"====(
+ var Source="";
+ var Source_data="";
+ var RMSextIP="";
+ var GS=String.fromCharCode(29); //Group Separator
+ var RS=String.fromCharCode(30); //Record Separator
+ var nomSondeFixe="Sonde Fixe";
+ var nomSondeMobile="Sonde Mobile";
+ var nomTemperature="Temperature";
+ function LoadParaRouteur() {
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ var LesParas=this.responseText;
+ var Para=LesParas.split(RS);
+ Source=Para[0];
+ Source_data=Para[1];
+ RMSextIP= Para[6];
+ AdaptationSource();
+ GH("nom_R",Para[2]);
+ GH("version",Para[3]);
+ GH("nomSondeFixe",Para[4]);
+ GH("nomSondeMobile",Para[5]);
+ nomSondeFixe=Para[4];
+ nomSondeMobile=Para[5];
+ nomTemperature=Para[7];
+
+ }
+ };
+ xhttp.open('GET', 'ParaRouteurAjax', true);
+ xhttp.send();
+ }
+ function GID(id) { return document.getElementById(id); };
+ function GH(id, T) {
+ if ( GID(id)){
+ GID(id).innerHTML = T; }
+ }
+ function GV(id, T) { GID(id).value = T; }
+ function clean(S){ //Remplace & et ? pour les envois au serveur
+ let res=S.replace(/\%/g,"%25");
+ res = res.replace(/\&/g, "%26");
+ res = res.replace(/\#/g, "%23");
+ res = res.replace(/\+/g, "%2B");
+ res=res.replace(/amp;/g,"");
+ return res.replace(/\?/g,"%3F");
+ }
+ function int2ip (V) {
+ var ipInt=parseInt(V);
+ return ( (ipInt>>>24) +'.' + (ipInt>>16 & 255) +'.' + (ipInt>>8 & 255) +'.' + (ipInt & 255) );
+ }
+ function ip2int(ip) {
+ ip=ip.trim();
+ return ip.split('.').reduce(function(ipInt, octet) { return (ipInt<<8) + parseInt(octet, 10)}, 0) >>> 0;
+ }
+
+)====";
\ No newline at end of file
diff --git "a/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf" "b/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf"
new file mode 100644
index 0000000..6c612f5
Binary files /dev/null and "b/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf" differ
diff --git a/docs/routers/Florent/triac_timer/triac_timer.cpp b/docs/routers/Florent/triac_timer/triac_timer.cpp
new file mode 100644
index 0000000..ccef097
--- /dev/null
+++ b/docs/routers/Florent/triac_timer/triac_timer.cpp
@@ -0,0 +1,375 @@
+#include "triac_timer.hpp"
+
+#include "driver/gpio.h"
+#include "hal/gpio_hal.h"
+#include "hal/gpio_ll.h"
+#include "hal/gpio_types.h"
+#include "soc/gpio_reg.h"
+
+#include "driver/timer.h"
+#include "hal/timer_ll.h"
+#include "soc/timer_group_reg.h"
+
+#include "esp_check.h"
+#include "hal/clk_tree_hal.h"
+#include "rom/ets_sys.h"
+#include "soc/soc.h"
+
+
+static const char* TAG = "triac-timer";
+
+
+// Triac pulse width, microseconds.
+#define PULSE_WIDTH_US ( 400 )
+
+// Minimum delay to reach the voltage required for a gate current of 30mA.
+// delay_us = asin((gate_resistor * gate_current) / grid_volt_max) / pi * period_us
+// delay_us = asin((330 * 0.03) / 325) / pi * 10000 = 97us
+#define PHASE_DELAY_MIN_US ( 90 )
+
+// Minimum time to lock-out the zero crossing once it is triggered, microseconds.
+// This is to avoid interrupts on the opposite edge when there's a significant slew rate.
+#define ZC_FILTER_US ( 2000 )
+
+// Hardware timer
+static_assert(CONFIG_TRIAC_TIMER_NUM <= SOC_TIMER_GROUP_TOTAL_TIMERS);
+#define TIMER_GRP ( CONFIG_TRIAC_TIMER_NUM / SOC_TIMER_GROUPS )
+#define TIMER_IDX ( CONFIG_TRIAC_TIMER_NUM % SOC_TIMER_GROUP_TIMERS_PER_GROUP )
+
+
+/*
+ * Low level API similar to "hal/gpio_ll.h" and "hal/timer_ll.h"
+ */
+
+#define INLINE_ATTR static inline __attribute__((always_inline))
+
+INLINE_ATTR void _gpio_ll_clear_outputs(uint64_t mask) {
+ REG_WRITE(GPIO_OUT_W1TC_REG, (uint32_t)mask); // GPIO0~31
+ #if SOC_GPIO_PIN_COUNT > 31
+ if ((uint32_t)(mask >> 32))
+ REG_WRITE(GPIO_OUT1_W1TC_REG, (uint32_t)(mask >> 32)); // GPIO32~39
+ #endif
+}
+
+INLINE_ATTR void _gpio_ll_set_outputs(uint64_t mask) {
+ REG_WRITE(GPIO_OUT_W1TS_REG, (uint32_t)mask); // GPIO0~31
+ #if SOC_GPIO_PIN_COUNT > 31
+ if ((uint32_t)(mask >> 32))
+ REG_WRITE(GPIO_OUT1_W1TS_REG, (uint32_t)(mask >> 32)); // GPIO32~39
+ #endif
+}
+
+
+#if TIMER_IDX == 0
+ #define TIMG_LOAD_REG TIMG_T0LOAD_REG(TIMER_GRP)
+ #define TIMG_LOADHI_REG TIMG_T0LOADHI_REG(TIMER_GRP)
+ #define TIMG_LOADLO_REG TIMG_T0LOADLO_REG(TIMER_GRP)
+ #define TIMG_UPDATE_REG TIMG_T0UPDATE_REG(TIMER_GRP)
+ #define TIMG_LO_REG TIMG_T0LO_REG(TIMER_GRP)
+ #define TIMG_ALARMHI_REG TIMG_T0ALARMHI_REG(TIMER_GRP)
+ #define TIMG_ALARMLO_REG TIMG_T0ALARMLO_REG(TIMER_GRP)
+ #define TIMG_CONFIG_REG TIMG_T0CONFIG_REG(TIMER_GRP)
+ #define TIMG_INT_CLR TIMG_T0_INT_CLR
+#else
+ #define TIMG_LOAD_REG TIMG_T1LOAD_REG(TIMER_GRP)
+ #define TIMG_LOADHI_REG TIMG_T1LOADHI_REG(TIMER_GRP)
+ #define TIMG_LOADLO_REG TIMG_T1LOADLO_REG(TIMER_GRP)
+ #define TIMG_UPDATE_REG TIMG_T1UPDATE_REG(TIMER_GRP)
+ #define TIMG_LO_REG TIMG_T1LO_REG(TIMER_GRP)
+ #define TIMG_ALARMHI_REG TIMG_T1ALARMHI_REG(TIMER_GRP)
+ #define TIMG_ALARMLO_REG TIMG_T1ALARMLO_REG(TIMER_GRP)
+ #define TIMG_CONFIG_REG TIMG_T1CONFIG_REG(TIMER_GRP)
+ #define TIMG_INT_CLR TIMG_T1_INT_CLR
+#endif
+
+INLINE_ATTR void _timer_ll_clear_intr_status() {
+ REG_WRITE(TIMG_INT_CLR_TIMERS_REG(TIMER_GRP), TIMG_INT_CLR);
+}
+
+INLINE_ATTR void _timer_ll_set_reload_value(uint32_t value) {
+ REG_WRITE(TIMG_LOADHI_REG, 0);
+ REG_WRITE(TIMG_LOADLO_REG, value);
+}
+
+INLINE_ATTR void _timer_ll_set_alarm_value(uint32_t value) {
+ REG_WRITE(TIMG_ALARMHI_REG, 0);
+ REG_WRITE(TIMG_ALARMLO_REG, value);
+}
+
+INLINE_ATTR void _timer_ll_reload() {
+ REG_WRITE(TIMG_LOAD_REG, 1);
+}
+
+INLINE_ATTR uint32_t _timer_ll_get_value() {
+ REG_WRITE(TIMG_UPDATE_REG, 1);
+ // Spin wait for the update as implemented by the SDK.
+ // We only need the lower 32bits so the wait could be useless if the lower registry is updated first.
+ while (REG_READ(TIMG_UPDATE_REG)) { }
+ return REG_READ(TIMG_LO_REG);
+}
+
+INLINE_ATTR void _timer_ll_set_alarm(uint32_t value) {
+ REG_WRITE(TIMG_ALARMLO_REG, value);
+ REG_SET_BIT(TIMG_CONFIG_REG, TIMG_T0_ALARM_EN | TIMG_T0_LEVEL_INT_EN);
+}
+
+INLINE_ATTR uint32_t _timer_ll_get_alarm() {
+ return REG_READ(TIMG_ALARMLO_REG);
+}
+
+
+/**
+ * AC phase delay table (sine square inverse CDF).
+ * power_ratio = sin(2 * pi * delay_ratio) / (2 * pi) - delay_ratio + 1
+ * index 0 ≠0% power ≠5% phase angle ≠95% firing delay
+ * index 39 ≠50% power ≠50% phase angle ≠50% firing delay
+ * index 79 ≠100% power ≠95% phase angle ≠5% firing delay
+ */
+
+static_assert(CONFIG_TRIAC_RESOLUTION <= 15);
+
+#define TABLE_PHASE_LEN ( 80U )
+#define TABLE_PHASE_SCALE ( (TABLE_PHASE_LEN - 1U) * (1UL << (16 - CONFIG_TRIAC_RESOLUTION)) )
+
+static const uint16_t TABLE_PHASE_DELAY[TABLE_PHASE_LEN] {
+ 0xefea, 0xdfd4, 0xd735, 0xd10d, 0xcc12, 0xc7cc, 0xc403, 0xc094,
+ 0xbd6a, 0xba78, 0xb7b2, 0xb512, 0xb291, 0xb02b, 0xaddc, 0xaba2,
+ 0xa97a, 0xa762, 0xa557, 0xa35a, 0xa167, 0x9f7f, 0x9da0, 0x9bc9,
+ 0x99fa, 0x9831, 0x966e, 0x94b1, 0x92f9, 0x9145, 0x8f95, 0x8de8,
+ 0x8c3e, 0x8a97, 0x88f2, 0x8750, 0x85ae, 0x840e, 0x826e, 0x80cf,
+ 0x7f31, 0x7d92, 0x7bf2, 0x7a52, 0x78b0, 0x770e, 0x7569, 0x73c2,
+ 0x7218, 0x706b, 0x6ebb, 0x6d07, 0x6b4f, 0x6992, 0x67cf, 0x6606,
+ 0x6437, 0x6260, 0x6081, 0x5e99, 0x5ca6, 0x5aa9, 0x589e, 0x5686,
+ 0x545e, 0x5224, 0x4fd5, 0x4d6f, 0x4aee, 0x484e, 0x4588, 0x4296,
+ 0x3f6c, 0x3bfd, 0x3834, 0x33ee, 0x2ef3, 0x28cb, 0x202c, 0x1016};
+
+
+static uint16_t lookup_phase_delay(uint32_t duty, uint16_t period)
+{
+ uint32_t slot = duty * TABLE_PHASE_SCALE + (TABLE_PHASE_SCALE >> 1);
+ uint16_t index = slot >> 16;
+ uint32_t a = TABLE_PHASE_DELAY[index];
+ uint32_t b = TABLE_PHASE_DELAY[index + 1];
+ uint32_t delay = a - (((a - b) * (slot & 0xffff)) >> 16); // interpolate a b
+
+ return (delay * period) >> 16; // scale to period
+}
+
+
+
+static Triac* _triac_root = NULL; // instance chaining
+static uint64_t _out_mask = 0; // mask for all the output pins
+static gpio_num_t _zc_pin = GPIO_NUM_NC; // zero crossing pin number
+static uint32_t _zc_delay = 0; // zero crossing delay, unit: 1us
+static volatile uint32_t _zc_period = 0; // zero crossing period, unit: 1us << 16
+
+
+static void IRAM_ATTR __isr_crossing(void *arg)
+{
+ uint32_t tick_now = _timer_ll_get_value();
+ uint32_t tick_next = -1;
+
+ // suspend zero-crossing interrupts to lock-out the ZC input
+ // uint32_t tick_next = ZC_FILTER_US;
+ // gpio_ll_set_intr_type(&GPIO, _zc_pin, GPIO_INTR_DISABLE);
+
+ if (tick_now > ZC_FILTER_US) // lock-out interval to avoid interrupts on the opposite edge
+ {
+ // reset timer and clear interrupts in case some are pending
+ _timer_ll_reload();
+ _timer_ll_clear_intr_status();
+
+ // turn off outputs in case some are still ON
+ _gpio_ll_clear_outputs(_out_mask);
+
+ // set each triac firing delay and pick the lowest for the alarm
+ for (Triac* triac = _triac_root; triac; triac = triac->_next)
+ {
+ uint32_t delay = triac->_delay;
+ triac->_alarm = delay;
+ triac->_triggered = false;
+
+ if (delay < tick_next)
+ tick_next = delay;
+ }
+
+ // set alarm
+ if ((int32_t)tick_next >= 0) {
+ _timer_ll_set_alarm(tick_next);
+ }
+
+ // update period, Exponential Moving Average N=16
+ int32_t avg = _zc_period; // 1us << 16
+ avg += ((int32_t)(tick_now << 16) - avg) >> 4;
+ _zc_period = avg;
+ }
+}
+
+
+static void IRAM_ATTR __isr_timer(void *arg)
+{
+ // Clear the interrupt (re-entrance occur when called at the end).
+ _timer_ll_clear_intr_status();
+
+ // Get elapsed time since zero crossing. Can use the timer value or alarm value.
+ // The alarm value is cheaper but less accurate if the interrupt is delayed.
+ uint32_t tick_now = _timer_ll_get_value();
+ // uint32_t tick_now = _timer_ll_get_alarm();
+ uint32_t tick_next = -1;
+
+ // ets_printf("%lu\n", tick_now);
+
+ // handle triacs with a lower alarm
+ for (Triac* triac = _triac_root; triac; triac = triac->_next)
+ {
+ uint32_t tick = triac->_alarm; // pulse ON or OFF delay from ZC
+
+ if (tick <= tick_now) {
+ // turn OFF pulse if ON
+ if (triac->_triggered) {
+ _gpio_ll_clear_outputs(triac->_mask);
+ triac->_alarm = -1; // turn off triac alarm
+ continue;
+ }
+ // turn ON pulse
+ _gpio_ll_set_outputs(triac->_mask);
+ tick = tick_now + PULSE_WIDTH_US; // add pulse width
+ triac->_triggered = true; // triggered flag
+ triac->_alarm = tick; // triac next alarm
+ }
+
+ // pick lowest tick for next alarm
+ if (tick < tick_next) {
+ tick_next = tick;
+ }
+ }
+
+ // set alarm, works even when set before the timer current value.
+ if ((int32_t)tick_next >= 0) {
+ _timer_ll_set_alarm(tick_next);
+ }
+
+ // restore zero-crossing interrupts
+ // gpio_ll_set_intr_type(&GPIO, _zc_pin, GPIO_INTR_POSEDGE);
+}
+
+
+bool Triac::begin(gpio_num_t sync_pin, uint16_t delay_us, bool invert)
+{
+ _zc_delay = delay_us;
+ _zc_pin = sync_pin;
+
+ // output pins
+
+ for (Triac* triac = _triac_root; triac; triac = triac->_next)
+ {
+ ESP_ERROR_CHECK(gpio_set_level(triac->_pin, 0));
+ ESP_ERROR_CHECK(gpio_reset_pin(triac->_pin));
+ ESP_ERROR_CHECK(gpio_set_direction(triac->_pin, GPIO_MODE_OUTPUT));
+ }
+
+ // general purpose timer
+
+ timer_config_t timer_cfg = { };
+ timer_cfg.alarm_en = TIMER_ALARM_DIS;
+ timer_cfg.counter_en = TIMER_START;
+ timer_cfg.intr_type = TIMER_INTR_LEVEL;
+ timer_cfg.counter_dir = TIMER_COUNT_UP;
+ timer_cfg.auto_reload = TIMER_AUTORELOAD_DIS;
+ timer_cfg.clk_src = TIMER_SRC_CLK_DEFAULT;
+ timer_cfg.divider = clk_hal_apb_get_freq_hz() / 1000000; // 1us tick
+
+ ESP_ERROR_CHECK(timer_init((timer_group_t)TIMER_GRP,
+ (timer_idx_t)TIMER_IDX,
+ &timer_cfg));
+
+ ESP_ERROR_CHECK(timer_isr_register((timer_group_t)TIMER_GRP,
+ (timer_idx_t)TIMER_IDX,
+ __isr_timer,
+ NULL,
+ ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM,
+ NULL));
+
+ ESP_ERROR_CHECK(timer_enable_intr((timer_group_t)TIMER_GRP,
+ (timer_idx_t)TIMER_IDX));
+
+ _timer_ll_set_reload_value(0);
+ _timer_ll_set_alarm_value(0);
+
+ // zero-crossing interrupt
+
+ ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM));
+
+ gpio_config_t io_cfg = { };
+ io_cfg.intr_type = invert ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE;
+ io_cfg.mode = GPIO_MODE_INPUT;
+ io_cfg.pin_bit_mask = (1ULL << sync_pin);
+ io_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
+ io_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
+ ESP_ERROR_CHECK(gpio_config(&io_cfg));
+
+ ESP_ERROR_CHECK(gpio_isr_handler_add(sync_pin, __isr_crossing, NULL));
+
+ // esp_intr_dump(NULL);
+
+ return true;
+}
+
+
+void Triac::end()
+{
+ gpio_isr_handler_remove(_zc_pin);
+ timer_deinit((timer_group_t)TIMER_GRP, (timer_idx_t)TIMER_IDX);
+ _gpio_ll_clear_outputs(_out_mask); // turn off triacs
+}
+
+
+uint32_t Triac::get_period_us()
+{
+ return (_zc_period + (1U << 14)) >> 15; // shift 16, x2, round
+}
+
+
+
+Triac::Triac(gpio_num_t pin) :
+ _next(_triac_root), // chain next instance
+ _pin(pin), // output pin number
+ _mask(1ULL << pin), // output pin mask
+ _delay(-1), // triac firing tick
+ _alarm(-1), // next tick for pulse start or pulse end
+ _triggered(false) // triggered flag
+{
+ _triac_root = this; // chain as root
+ _out_mask |= _mask; // add to outputs mask
+}
+
+
+void Triac::set(int32_t value)
+{
+ if (value <= 0) {
+ _delay = UINT32_MAX; // OFF 0%
+ this->value = 0;
+ }
+ else if (value >= TRIAC_MAX) {
+ _delay = _zc_delay + PHASE_DELAY_MIN_US; // ON 100%
+ this->value = TRIAC_MAX;
+ }
+ else {
+ _delay = _zc_delay + lookup_phase_delay(value, _zc_period >> 16); // dimmed
+ this->value = value;
+ }
+}
+
+
+int32_t Triac::add(int32_t value)
+{
+ int32_t v = this->value;
+ set(v + value);
+ return this->value - v;
+}
+
+
+void Triac::test_zc()
+{
+ _delay = _zc_delay;
+}
diff --git a/docs/routers/Florent/triac_timer/triac_timer.hpp b/docs/routers/Florent/triac_timer/triac_timer.hpp
new file mode 100644
index 0000000..c12a497
--- /dev/null
+++ b/docs/routers/Florent/triac_timer/triac_timer.hpp
@@ -0,0 +1,141 @@
+/**
+ * Low level phase control driver implemented with a Timer Group (TIMG).
+ *
+ * 12 bits resolution by default (0 to 4095).
+ * Detects automatically the frequency of the grid.
+ * The output power is linear (sine square distribution).
+ * No limitation on the number of output.
+ * The zero crossing input is filtered.
+ * Designed for minimum CPU usage and low latency.
+ * Uses integers only, no floating points, no FPU lock.
+ *
+ * Drawbacks:
+ * Interrupts adds a 5us delay between ZC input and output.
+ * Interrupts may be delayed under heavy load.
+ *
+ * Usage:
+ *
+ * #include "triac-timer.hpp"
+ *
+ * #define PIN_ZEROCROSS ( 16 )
+ * #define PIN_TRIAC_1 ( 17 )
+ * #define PIN_TRIAC_2 ( 18 )
+ * #define ZC_DELAY_US ( 170 )
+ *
+ * Triac triac1(PIN_TRIAC_1);
+ * Triac triac2(PIN_TRIAC_2);
+ *
+ * void setup()
+ * {
+ * Triac::begin(PIN_ZEROCROSS, ZC_DELAY_US);
+ * }
+ *
+ * void loop()
+ * {
+ * triac1.set(100 * TRIAC_MAX / 100); // 100%
+ * triac2.set(75 * TRIAC_MAX / 100); // 75%
+ * delay(500);
+ * triac1.set(0 * TRIAC_MAX / 100); // 0%
+ * triac2.set(25 * TRIAC_MAX / 100); // 25%
+ * delay(500);
+ * }
+ *
+ * TODO:
+ * check interrupt priority gpio >= timer and occur on same core
+ * check watchdog timer
+ *
+ */
+
+#pragma once
+
+#include "hal/gpio_types.h"
+#include
+#include
+
+/**
+ * Optional timer, 0-3 for most ESP32, 0-1 for ESP32-C3
+ */
+#ifndef CONFIG_TRIAC_TIMER_NUM
+#define CONFIG_TRIAC_TIMER_NUM ( 0 )
+#endif
+
+/**
+ * Optional resolution, 15bits max
+ */
+#ifndef CONFIG_TRIAC_RESOLUTION
+#define CONFIG_TRIAC_RESOLUTION ( 12 )
+#endif
+
+
+#define TRIAC_MAX ( (1 << CONFIG_TRIAC_RESOLUTION) - 1 )
+
+
+struct Triac
+{
+ Triac* _next; // next triac instance
+ gpio_num_t _pin; // output pin number
+ uint64_t _mask; // output mask
+ uint32_t _delay; // triac firing delay, us
+
+ volatile uint32_t _alarm; // alarm tick for pulse start or pulse end
+ volatile bool _triggered; // ON/OFF flag
+
+ uint32_t value; // 12 bits duty, 0-4095, readonly
+
+
+ /**
+ * @brief Constructor
+ *
+ * @param pin Output pin number
+ */
+ Triac(gpio_num_t pin);
+
+ /**
+ * @brief Set the output duty/power.
+ * Must wait at least 2 seconds after calling Triac::begin
+ *
+ * @param value 12 bits from 0 (OFF) to 4095 (Fully ON).
+ */
+ void set(int32_t value);
+
+ /**
+ * @brief Increase or decrease the output duty/power.
+ *
+ * @param value 12 bits, +/-4095 (+/-100%)
+ * @return Added power within available range.
+ */
+ int32_t add(int32_t value);
+
+ /**
+ * @brief Outputs the zero crossing signal for calibration.
+ */
+ void test_zc();
+
+
+
+ /**
+ * @brief Configure and start the phase controller.
+ *
+ * @param sync_pin Zero crossing input pins, GPIO number.
+ * @param delay_us Zero crossing delay, microseconds.
+ * @param invert Invert the zero crossing input, false: high pulse, true: low pulse.
+ */
+ static bool begin(gpio_num_t sync_pin, uint16_t delay_us = 170, bool invert = false);
+
+ /**
+ * @brief Stop the phase controller and free resources.
+ */
+ static void end();
+
+ /**
+ * @brief Grid period, moving average, microsecond.
+ *
+ * @example
+ *
+ * uint32_t period_us = Triac::get_period_us();
+ * float frequency_hz = period_us ? (1000000.0 / period_us) : 0;
+ *
+ */
+ static uint32_t get_period_us();
+
+};
diff --git a/docs/routers/FredM67-PVRouter-1-phase b/docs/routers/FredM67-PVRouter-1-phase
new file mode 160000
index 0000000..9678c01
--- /dev/null
+++ b/docs/routers/FredM67-PVRouter-1-phase
@@ -0,0 +1 @@
+Subproject commit 9678c018e0bb8af5e19d021baf8d94aff3d555a9
diff --git a/docs/routers/FredM67-PVRouter-3-phase b/docs/routers/FredM67-PVRouter-3-phase
new file mode 160000
index 0000000..42da896
--- /dev/null
+++ b/docs/routers/FredM67-PVRouter-3-phase
@@ -0,0 +1 @@
+Subproject commit 42da89637e99a66feb2e88df09ba4c0deb0885c3
diff --git a/docs/routers/Jetblack31-MaxPV b/docs/routers/Jetblack31-MaxPV
new file mode 160000
index 0000000..9db686b
--- /dev/null
+++ b/docs/routers/Jetblack31-MaxPV
@@ -0,0 +1 @@
+Subproject commit 9db686b093b7d4527a27ecd87d3f88c320d4af9a
diff --git a/docs/routers/LSA/Explication Solar PID_fr.pdf b/docs/routers/LSA/Explication Solar PID_fr.pdf
new file mode 100644
index 0000000..713ea4d
Binary files /dev/null and b/docs/routers/LSA/Explication Solar PID_fr.pdf differ
diff --git "a/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf" "b/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf"
new file mode 100644
index 0000000..6ee5eac
Binary files /dev/null and "b/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf" differ
diff --git a/docs/routers/LSA/Programmation_ESP32.pdf b/docs/routers/LSA/Programmation_ESP32.pdf
new file mode 100644
index 0000000..b295880
Binary files /dev/null and b/docs/routers/LSA/Programmation_ESP32.pdf differ
diff --git a/docs/routers/LSA/Regler_pwm_will/.gitignore b/docs/routers/LSA/Regler_pwm_will/.gitignore
new file mode 100644
index 0000000..89cc49c
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd.txt
new file mode 100644
index 0000000..7d385d4
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kd.txt
@@ -0,0 +1 @@
+0.25
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt
new file mode 100644
index 0000000..7d385d4
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt
@@ -0,0 +1 @@
+0.25
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt
new file mode 100644
index 0000000..7d385d4
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt
@@ -0,0 +1 @@
+0.25
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki.txt
new file mode 100644
index 0000000..e9b8f99
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Ki.txt
@@ -0,0 +1 @@
+0.05
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt
new file mode 100644
index 0000000..e9b8f99
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt
@@ -0,0 +1 @@
+0.05
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt
new file mode 100644
index 0000000..3b04cfb
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt
@@ -0,0 +1 @@
+0.2
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kp.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt
new file mode 100644
index 0000000..49d5957
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt
@@ -0,0 +1 @@
+0.1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt b/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt
new file mode 100644
index 0000000..83b33d2
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt
@@ -0,0 +1 @@
+1000
diff --git a/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt b/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt
new file mode 100644
index 0000000..3cacc0b
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt
@@ -0,0 +1 @@
+12
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt
@@ -0,0 +1 @@
+2
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt
new file mode 100644
index 0000000..64bb6b7
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt
@@ -0,0 +1 @@
+30
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt
new file mode 100644
index 0000000..abdfb05
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt
@@ -0,0 +1 @@
+60
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt
new file mode 100644
index 0000000..f599e28
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt
@@ -0,0 +1 @@
+10
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt
new file mode 100644
index 0000000..209e3ef
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt
@@ -0,0 +1 @@
+20
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt
new file mode 100644
index 0000000..209e3ef
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt
@@ -0,0 +1 @@
+20
diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt
new file mode 100644
index 0000000..209e3ef
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt
@@ -0,0 +1 @@
+20
diff --git a/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt b/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt b/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt
new file mode 100644
index 0000000..3b04cfb
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt
@@ -0,0 +1 @@
+0.2
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt
new file mode 100644
index 0000000..3b04cfb
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt
@@ -0,0 +1 @@
+0.2
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt
new file mode 100644
index 0000000..aec258d
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt
@@ -0,0 +1 @@
+0.8
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt
new file mode 100644
index 0000000..a92d21c
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt
@@ -0,0 +1 @@
+.3
diff --git a/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt
new file mode 100644
index 0000000..08839f6
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt
@@ -0,0 +1 @@
+200
diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt
new file mode 100644
index 0000000..eb1f494
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt
@@ -0,0 +1 @@
+500
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt
new file mode 100644
index 0000000..3d4c7bf
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt
@@ -0,0 +1 @@
+220
diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt
new file mode 100644
index 0000000..83be903
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt
@@ -0,0 +1 @@
+570
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt b/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt
new file mode 100644
index 0000000..ace9d03
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt
@@ -0,0 +1 @@
+255
diff --git a/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt b/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt
new file mode 100644
index 0000000..ace9d03
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt
@@ -0,0 +1 @@
+255
diff --git a/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt b/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt b/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt
@@ -0,0 +1 @@
+1
diff --git a/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt
@@ -0,0 +1 @@
+0
diff --git a/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt b/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt
new file mode 100644
index 0000000..60d3b2f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt
@@ -0,0 +1 @@
+15
diff --git a/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt b/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt
new file mode 100644
index 0000000..fa8f08c
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt
@@ -0,0 +1 @@
+150
diff --git a/docs/routers/LSA/Regler_pwm_will/data/www/index.html b/docs/routers/LSA/Regler_pwm_will/data/www/index.html
new file mode 100644
index 0000000..2a4efb4
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/data/www/index.html
@@ -0,0 +1,153 @@
+
+
+
+
+ Regler Werte Eingabe
+
+
+
+
+
+
+
+ //******************************* SSR-1 ***************************************//
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ //******************************* SSR-2 ***************************************//
+
+
+
+
+
+
+
+
+
+
+ //******************************* Einschalten Boiler, Heizung ***************************//
+
+
+
+
+
+
+
+
+
+
+
+
+ //******************************* Testwerte ***************************//
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py b/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py
new file mode 100644
index 0000000..93937e2
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py
@@ -0,0 +1,2 @@
+Import("env")
+env.Replace( MKSPIFFSTOOL=env.get("PROJECT_DIR") + '/mklittlefs' )
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe b/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe
new file mode 100644
index 0000000..aa52570
Binary files /dev/null and b/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe differ
diff --git a/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc b/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc
new file mode 100644
index 0000000..b7ab7fa
Binary files /dev/null and b/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc differ
diff --git a/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py b/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py
new file mode 100644
index 0000000..30acc4e
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+'''
+ Copyright (c)
+
+ 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 subprocess
+import socket
+import os
+import signal
+import sys
+from platformio.commands.device import DeviceMonitorFilter
+from platformio.project.config import ProjectConfig
+
+PORT = 19200
+
+class SerialPlotter(DeviceMonitorFilter):
+ NAME = "plotter"
+
+ def __init__(self, *args, **kwargs):
+ super(SerialPlotter, self).__init__(*args, **kwargs)
+ self.buffer = ''
+ self.arduplot = 'arduplot'
+ self.plot = None
+ self.plot_sock = ''
+ self.plot = ''
+
+ def __call__(self):
+ pio_root = ProjectConfig.get_instance().get_optional_dir("core")
+ if sys.platform == 'win32':
+ self.arduplot = os.path.join(pio_root, 'penv', 'Scripts' , self.arduplot + '.cmd')
+ else:
+ self.arduplot = os.path.join(pio_root, 'penv', 'bin' , self.arduplot)
+ print('--- serial_plotter is starting')
+ self.plot = subprocess.Popen([self.arduplot, '-s', str(PORT)])
+ try:
+ self.plot_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.plot_sock.connect(('localhost', PORT))
+ except socket.error:
+ pass
+ return self
+
+ def __del__(self):
+ if self.plot:
+ if sys.platform == 'win32':
+ self.plot.send_signal(signal.CTRL_C_EVENT)
+ self.plot.kill()
+
+ def rx(self, text):
+ if self.plot.poll() is None: # None means the child is running
+ self.buffer += text
+ if '\n' in self.buffer:
+ try:
+ self.plot_sock.send(bytes(self.buffer, 'utf-8'))
+ except BrokenPipeError:
+ try:
+ self.plot_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.plot_sock.connect(('localhost', PORT))
+ except socket.error:
+ pass
+ self.buffer = ''
+ else:
+ os.kill(os.getpid(), signal.SIGINT)
+ return text
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv b/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv
new file mode 100644
index 0000000..97846fa
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv
@@ -0,0 +1,6 @@
+# Name, Type, SubType, Offset, Size, Flags
+ota_0, app, ota_0, 0x10000, 0x1A0000,
+ota_1, app, ota_1, , 0x1A0000,
+otadata, data, ota, 0x350000, 0x2000,
+nvs, data, nvs, , 0x6000,
+data, data, spiffs, , 0xA8000,
diff --git a/docs/routers/LSA/Regler_pwm_will/platformio.ini b/docs/routers/LSA/Regler_pwm_will/platformio.ini
new file mode 100644
index 0000000..75d10f6
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/platformio.ini
@@ -0,0 +1,38 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:esp32doit-devkit-v1]
+
+
+platform = espressif32
+board = esp32doit-devkit-v1
+build_flags =
+ -DASYNCWEBSERVER_REGEX
+framework = arduino
+
+
+monitor_speed = 115200
+board_build.filesystem = littlefs
+lib_deps =
+ miq19/eModbus@^1.4.1
+ knolleary/PubSubClient@^2.8
+ me-no-dev/ESP Async WebServer@^1.2.3
+ me-no-dev/AsyncTCP@^1.1.1
+ bblanchon/ArduinoJson@^6.19.4
+
+ dlloydev/ESP32 ESP32S2 AnalogWrite@^2.0.9
+ dlloydev/QuickPID@^3.1.2
+ dlloydev/sTune@^2.4.0
+
+
+ thomasfredericks/Bounce2@^2.71
+ Wire
+ jandrassy/ArduinoOTA@^1.0.8
+ dlloydev/sTune@^2.4.0
diff --git a/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace b/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace
new file mode 100644
index 0000000..bab1b7f
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": ".."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/src/credentials.h b/docs/routers/LSA/Regler_pwm_will/src/credentials.h
new file mode 100644
index 0000000..c406bcc
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/credentials.h
@@ -0,0 +1,15 @@
+
+
+
+#define MY_SSID "Fritzbox"
+#define MY_PASS "Passwort_eingeben"
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/routers/LSA/Regler_pwm_will/src/main.cpp b/docs/routers/LSA/Regler_pwm_will/src/main.cpp
new file mode 100644
index 0000000..063bf15
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/main.cpp
@@ -0,0 +1,2104 @@
+
+/*
+https://github.com/lorol/LITTLEFS/issues/43
+LITTLEFSimpl::exists() bug with Platformio (ESP32 arduino framework) in Visual Studio Code
+Fix by adding third argument
+File f = open(path, "r", false);
+
+line 44 in LITTLEFS.cpp
+
+also found
+https://github.com/espressif/arduino-esp32/pull/6179
+
+With LittleFS the `fs.exists(path)` returns true also on folders. A `isDirectory()` call is required to set _isFile to false on directories.
+This enables serving all files from a folder like : `server->serveStatic("/", LittleFS, "/", cacheHeader.c_str());
+ File f = fs.open(path);
+ _isFile = (f && (! f.isDirectory()));
+
+line 1120
+*/
+
+// Includes: for Serial etc., WiFi.h for WiFi support
+#include
+#include
+
+// for Smarty
+#include
+#include
+
+// Webserver OTA
+
+#include
+
+#include
+
+#include
+
+// #include
+#include
+#include "LITTLEFS.h"
+
+// PID
+#include
+#include
+
+// stune
+
+#include
+
+// Include the header for the ModbusClient TCP style
+#include
+
+#include "credentials.h"
+
+// #define BUILTIN_LED 2
+#define Alarm 27
+#define SSR_1 26
+// #define SSR_1 13
+#define SSR_2 22
+
+///////////////////
+
+// #include
+
+#include "special_settings.h"
+// switch on (1)or of (0) serial.print = serial.print replaced by debug //
+// #define DEBUG 1 in special special_settings.h
+
+#if DEBUG == 1
+#define debug(x) Serial.print(x)
+#define debugln(x) Serial.println(x)
+#else
+#define debug(x)
+#define debugln(x)
+#endif
+
+#if DEBUGf == 1
+#define debugf(x) Serial.printf(x)
+#define debugfln(x) Serial.printfln(x)
+#else
+#define debugf(x)
+#define debugfln(x)
+#endif
+
+#if MQTT_DEBUG == 1
+#define mqtt_debug(x) Serial.print(x)
+#define mqtt_debugln(x) Serial.println(x)
+#else
+#define mqtt_debug(x)
+#define mqtt_debugln(x)
+#endif
+
+#if PID_DEBUG == 1
+#define pid_debug(x) Serial.print(x)
+#define pid_debugln(x) Serial.println(x)
+#else
+#define pid_debug(x)
+#define pid_debugln(x)
+#endif
+
+#if SSR_DEBUG == 1
+#define ssr_debug(x) Serial.print(x)
+#define ssr_debugln(x) Serial.println(x)
+#else
+#define ssr_debug(x)
+#define ssr_debugln(x)
+#endif
+
+#if SMARTY_DEBUG == 1
+#define smarty_debug(x) Serial.print(x)
+#define smarty_debugln(x) Serial.println(x)
+#else
+#define smarty_debug(x)
+#define smarty_debugln(x)
+#endif
+
+#if MODBUS_DEBUG == 1
+#define modbus_debug(x) Serial.print(x)
+#define modbus_debugln(x) Serial.println(x)
+#else
+#define modbus_debug(x)
+#define modbus_debugln(x)
+#endif
+
+#if I2C_DEBUG == 1
+#define i2c -debug(x) Serial.print(x)
+#define i2c_debugln(x) Serial.println(x)
+#else
+#define i2c_debug(x)
+#define i2c_debugln(x)
+#endif
+
+#if PWM_DEBUG == 1
+#define pwm_debug(x) Serial.print(x)
+#define pwm_debugln(x) Serial.println(x)
+#else
+#define pwm_debug(x)
+#define pwm_debugln(x)
+#endif
+
+#if DFPLAYER_DEBUG == 1
+#define dfplayer_debug(x) Serial.print(x)
+#define dfplayer_debugln(x) Serial.println(x)
+#else
+#define dfplayer_debug(x)
+#define dfplayer_debugln(x)
+#endif
+
+////////////////////
+
+uint16_t Kostal_Pow_L1;
+uint16_t Kostal_Pow_L2;
+uint16_t Kostal_Pow_L3;
+uint16_t Kostal_Pow_tot;
+
+uint16_t Kostal_Pow_L1_L2;
+uint16_t Kostal_Pow_L1_L3;
+uint16_t Kostal_Pow_L2_L3;
+
+uint16_t Basic_load_L1;
+uint16_t Basic_load_L2;
+uint16_t Basic_load_L3;
+
+uint16_t Basic_load_tot;
+/*
+int16_t int_Voltaik_Surplus_L1;
+int16_t int_Voltaik_Surplus_L2;
+int16_t int_Voltaik_Surplus_L3;
+int16_t int_Voltaik_Surplus_tot;
+*/
+
+float float_Voltaik_Surplus_L1;
+float float_Voltaik_Surplus_L2;
+float float_Voltaik_Surplus_L3;
+float float_Voltaik_Surplus_tot;
+
+uint32_t maxInflightRequests = 1;
+// FĂĽr Smarty
+float var_Will = 0;
+float var_Bezug_tot = 0;
+float var_Einsp_tot = 0;
+float var_Einsp_L1 = 0;
+float var_Einsp_L2 = 0;
+float var_Einsp_L3 = 0;
+
+float val_Bezug_tot = 0;
+float val_Einsp_tot = 0;
+float val_Einsp_L1 = 0;
+float val_Einsp_L2 = 0;
+float val_Einsp_L3 = 0;
+
+int val_Surplus = 0;
+int val_inverter = 0;
+
+float val_Excess_SSR_1 = 0;
+float val_Excess_SSR_2 = 0;
+
+// Warmwasser und Heizung Temperaturen
+float var_lower_temp_1 = 0;
+float var_upper_temp_1 = 0;
+float var_max_temp_1 = 0;
+
+float var_lower_temp_2 = 0;
+float var_upper_temp_2 = 0;
+float var_max_temp_2 = 0;
+
+// map range
+
+int min_range_1 = 10;
+int max_range_1 = 20;
+
+int min_range_2 = 10;
+int max_range_2 = 20;
+
+int val_SSR_1 = 0;
+int val_SSR_2 = 0;
+
+// Solid State Relais
+
+float SSR_1_on;
+float SSR_1_off;
+float SSR_2_on;
+float SSR_2_off;
+
+// PWM
+float Var_PWM_1;
+
+// PID
+
+float Setpoint_4;
+float aggKp;
+float aggKi;
+float aggKd;
+float consKp;
+float consKi;
+float consKd;
+float Input_4;
+float Output_4;
+
+float Setpoint_1;
+float aggKp_1;
+float aggKi_1;
+float aggKd_1;
+float consKp_1;
+float consKi_1;
+float consKd_1;
+float Input_1;
+float Output_1;
+
+float Setpoint_2;
+float aggKp_2;
+float aggKi_2;
+float aggKd_2;
+float consKp_2;
+float consKi_2;
+float consKd_2;
+float Input_2;
+float Output_2;
+
+float Input, Output, Setpoint = 50, Kp, Ki, Kd;
+
+QuickPID myPID_4(&Input_4, &Output_4, &Setpoint_4);
+QuickPID myPID_1(&Input_1, &Output_1, &Setpoint_1);
+QuickPID myPID_2(&Input_2, &Output_2, &Setpoint_2);
+
+sTune tuner = sTune(&Input, &Output, tuner.ZN_PID, tuner.directIP, tuner.printOFF);
+
+QuickPID myPID(&Input, &Output, &Setpoint);
+
+// values for teting
+
+float test_val_1;
+float test_val_2;
+
+// webpage Input
+
+const char *PARAM_INPUT_1 = "input_1";
+const char *PARAM_INPUT_2 = "input_2";
+const char *PARAM_INPUT_3 = "input_3";
+const char *PARAM_INPUT_4 = "input_4";
+
+const char *PARAM_INT_1 = "inputInt_1";
+const char *PARAM_INT_2 = "inputInt_2";
+const char *PARAM_INT_3 = "inputInt_3";
+const char *PARAM_INT_4 = "inputInt_4";
+
+const char *PARAM_Setpoint = "Setpoint";
+const char *PARAM_Setpoint_1 = "Setpoint_1";
+const char *PARAM_Setpoint_2 = "Setpoint_2";
+
+const char *PARAM_Kp = "Kp";
+const char *PARAM_Ki = "Ki";
+const char *PARAM_Kd = "Kd";
+
+const char *PARAM_aggKp = "aggKp";
+const char *PARAM_aggKi = "aggKi";
+const char *PARAM_aggKd = "aggKd";
+
+const char *PARAM_Kp_1 = "Kp_1";
+const char *PARAM_Ki_1 = "Ki_1";
+const char *PARAM_Kd_1 = "Kd_1";
+
+const char *PARAM_aggKp_1 = "aggKp_1";
+const char *PARAM_aggKi_1 = "aggKi_1";
+const char *PARAM_aggKd_1 = "aggKd_1";
+
+const char *PARAM_Kp_2 = "Kp_2";
+const char *PARAM_Ki_2 = "Ki_2";
+const char *PARAM_Kd_2 = "Kd_2";
+
+const char *PARAM_aggKp_2 = "aggKp_2";
+const char *PARAM_aggKi_2 = "aggKi_2";
+const char *PARAM_aggKd_2 = "aggKd_2";
+
+const char *PARAM_SSR_1_on = "SSR_1_on";
+const char *PARAM_SSR_2_on = "SSR_2_on";
+const char *PARAM_SSR_1_off = "SSR_1_off";
+const char *PARAM_SSR_2_off = "SSR_2_off";
+
+const char *PARAM_SSR_1_method = "SSR_1_method";
+const char *PARAM_SSR_2_method = "SSR_2_method";
+
+const char *PARAM_SSR_1_mode = "SSR_1_mode";
+const char *PARAM_SSR_2_mode = "SSR_2_mode";
+
+const char *PARAM_min_range_1 = "min_range_1";
+const char *PARAM_min_range_2 = "min_range_2";
+const char *PARAM_max_range_1 = "max_range_1";
+const char *PARAM_max_range_2 = "max_range_2";
+
+const char *PARAM_select_Input_SSR_2 = "select_Input_SSR_2";
+const char *PARAM_select_Input_SSR_1 = "select_Input_SSR_1";
+
+const char *PARAM_PWM_Freq = "PWM_Freq";
+const char *PARAM_PWM_Resolution = "PWM_Resolution";
+
+const char *PARAM_SSR_1_setpoint_distance = "SSR_1_setpoint_distance";
+const char *PARAM_SSR_1_PID_direction = "SSR_1_PID_direction";
+
+const char *PARAM_STRING = "inputString";
+const char *PARAM_INT = "inputInt";
+const char *PARAM_FLOAT = "inputFloat";
+
+const char *PARAM_upper_1 = "upper_1";
+const char *PARAM_lower_1 = "lower_1";
+const char *PARAM_max_1 = "max_1";
+
+const char *PARAM_upper_2 = "upper_2";
+const char *PARAM_lower_2 = "lower_2";
+const char *PARAM_max_2 = "max_2";
+
+const char *PARAM_test_val_1 = "test_val_1";
+const char *PARAM_test_val_2 = "test_val_2";
+// end webpage
+
+// MQTT settings see credentials.h
+
+// Smarty subscriptions see specialsettings.h
+
+WiFiClient ESP32_Client;
+PubSubClient MQTT_Client(ESP32_Client);
+StaticJsonDocument<64> doc;
+
+int msg = 0;
+int msg_L1 = 0;
+int msg_L2 = 0;
+int msg_L3 = 0;
+int msg_Exp = 0;
+
+// Warmwater and heater subscriptions see specialsettings.h
+
+int msg_upper_1 = 0;
+int msg_lower_1 = 0;
+int msg_max_1 = 0;
+
+int msg_upper_2 = 0;
+int msg_lower_2 = 0;
+int msg_max_2 = 0;
+
+// LITTLEFS
+unsigned int totalBytes = LITTLEFS.totalBytes();
+unsigned int usedBytes = LITTLEFS.usedBytes();
+
+// Webserver for parameter data input
+
+AsyncWebServer httpServer(80);
+
+void notFound(AsyncWebServerRequest *request)
+{
+ request->send(404, "text/plain", "Not found");
+}
+
+String readFile(fs::FS &fs, const char *path)
+{
+ // Serial.printf("Reading file: %s\r\n", path);
+ File file = fs.open(path, "r");
+ if (!file || file.isDirectory())
+ {
+ debugln("- empty file or failed to open file");
+ return String();
+ }
+ // debugln("- read from file:");
+ String fileContent;
+ while (file.available())
+ {
+ fileContent += String((char)file.read());
+ }
+ file.close();
+ // debugln(fileContent);
+ return fileContent;
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message)
+{
+ Serial.printf("Writing file: %s\r\n", path);
+ File file = fs.open(path, "w");
+ if (!file)
+ {
+ debugln("- failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ debugln("- file written");
+ }
+ else
+ {
+ debugln("- write failed");
+ }
+ file.close();
+}
+
+// Replaces placeholder with stored values
+
+String processor(const String &var)
+{
+ debugln(var);
+ debug("var");
+ debugln(var);
+ if (var == "inputString")
+ {
+ return readFile(LITTLEFS, "/inputString.txt");
+ }
+ else if (var == "inputInt")
+ {
+ return readFile(LITTLEFS, "/inputInt.txt");
+ }
+ else if (var == "inputFloat")
+ {
+ return readFile(LITTLEFS, "/inputFloat.txt");
+ }
+ else if (var == "inputInt_1")
+ {
+ return readFile(LITTLEFS, "/inputInt_1.txt");
+ }
+ else if (var == "inputInt_2")
+ {
+ return readFile(LITTLEFS, "/inputInt_2.txt");
+ }
+ else if (var == "inputInt_3")
+ {
+ return readFile(LITTLEFS, "/inputInt_3.txt");
+ }
+ else if (var == "inputInt_4")
+ {
+ return readFile(LITTLEFS, "/inputInt_4.txt");
+ }
+ /////////////
+
+ else if (var == "Setpoint")
+ {
+ return readFile(LITTLEFS, "/Setpoint.txt");
+ }
+
+ else if (var == "Setpoint_1")
+ {
+ return readFile(LITTLEFS, "/Setpoint_1.txt");
+ }
+ else if (var == "Setpoint_2")
+ {
+ return readFile(LITTLEFS, "/Setpoint_2.txt");
+ }
+ /////////
+ else if (var == "Kp")
+ {
+ return readFile(LITTLEFS, "*/Kp.txt");
+ }
+ else if (var == "Ki")
+ {
+ return readFile(LITTLEFS, "Ki.txt");
+ }
+ else if (var == "Kd")
+ {
+ return readFile(LITTLEFS, "/Kd.txt");
+ }
+ else if (var == "aggKp")
+ {
+ return readFile(LITTLEFS, "/aggKp.txt");
+ }
+ else if (var == "aggKi")
+ {
+ return readFile(LITTLEFS, "/aggKi.txt");
+ }
+ else if (var == "aggKd")
+ {
+ return readFile(LITTLEFS, "/aggKd.txt");
+ }
+
+ /////////
+ else if (var == "Kp_1")
+ {
+ return readFile(LITTLEFS, "/Kp_1.txt");
+ }
+ else if (var == "Ki_1")
+ {
+ return readFile(LITTLEFS, "/Ki_1.txt");
+ }
+ else if (var == "Kd_1")
+ {
+ return readFile(LITTLEFS, "/Kd_1.txt");
+ }
+ else if (var == "aggKp_1")
+ {
+ return readFile(LITTLEFS, "agg/Kp_1.txt");
+ }
+ else if (var == "aggKi_1")
+ {
+ return readFile(LITTLEFS, "/aggKi_1.txt");
+ }
+ else if (var == "aggKd_1")
+ {
+ return readFile(LITTLEFS, "/aggKd_1.txt");
+ }
+ ////////////
+ else if (var == "Kp_2")
+ {
+ return readFile(LITTLEFS, "/Kp_2.txt");
+ }
+ else if (var == "Ki_2")
+ {
+ return readFile(LITTLEFS, "/Ki_2.txt");
+ }
+ else if (var == "Kd_2")
+ {
+ return readFile(LITTLEFS, "/Kd_2.txt");
+ }
+
+ else if (var == "aggKp_2")
+ {
+ return readFile(LITTLEFS, "/aggKp_2.txt");
+ }
+ else if (var == "aggKi_2")
+ {
+ return readFile(LITTLEFS, "/aggKi_2.txt");
+ }
+ else if (var == "aggKd_2")
+ {
+ return readFile(LITTLEFS, "/aggKd_2.txt");
+ }
+ /////////////////////////////
+ else if (var == "SSR_1_method")
+ {
+ return readFile(LITTLEFS, "/SSR_1_method.txt");
+ }
+ else if (var == "SSR_2_method")
+ {
+ return readFile(LITTLEFS, "/SSR_2_method.txt");
+ }
+
+ /////////////////////////////
+ else if (var == "SSR_1_mode")
+ {
+ return readFile(LITTLEFS, "/SSR_1_mode.txt");
+ }
+ else if (var == "SSR_2_mode")
+ {
+ return readFile(LITTLEFS, "/SSR_2_mode.txt");
+ }
+
+ else if (var == "select_Input_SSR_1")
+ {
+ return readFile(LITTLEFS, "/select_Input_SSR_1.txt");
+ }
+ else if (var == "select_Input_SSR_2")
+ {
+ return readFile(LITTLEFS, "/select_Input_SSR_2.txt");
+ }
+ /////////////////////////////
+
+ //////////
+
+ else if (var == "min_range_1")
+ {
+ return readFile(LITTLEFS, "/min_range_1.txt");
+ }
+ else if (var == "min_range_2")
+ {
+ return readFile(LITTLEFS, "/min_range_2.txt");
+ }
+ else if (var == "max_range_1")
+ {
+ return readFile(LITTLEFS, "/max_range_1.txt");
+ }
+ else if (var == "max_range_2")
+ {
+ return readFile(LITTLEFS, "/max_range_2.txt");
+ }
+
+ /////////////////////////////
+ else if (var == "SSR_1_on")
+ {
+ return readFile(LITTLEFS, "/SSR_1_on.txt");
+ }
+ else if (var == "SSR_2_on")
+ {
+ return readFile(LITTLEFS, "/SSR_2_on.txt");
+ }
+
+ /////////////////////////////
+ else if (var == "SSR_1_off")
+ {
+ return readFile(LITTLEFS, "/SSR_1_off.txt");
+ }
+ else if (var == "SSR_2_off")
+ {
+ return readFile(LITTLEFS, "/SSR_2_off.txt");
+ }
+
+ else if (var == "SSR_1_PID_direction")
+ {
+ return readFile(LITTLEFS, "/SSR_1_PID_direction.txt");
+ }
+ else if (var == "SSR_2_PID_direction")
+ {
+ return readFile(LITTLEFS, "/SSR_2_PID_direction.txt");
+ }
+ ///////////////
+ else if (var == "PWM_Freq.txt")
+ {
+ return readFile(LITTLEFS, "/PWM_Freq.txt");
+ }
+ else if (var == "PWM_Resolution.txt")
+ {
+ return readFile(LITTLEFS, "/PWM_Resolution.txt");
+ }
+
+ else if (var == "action_1.txt")
+ {
+ return readFile(LITTLEFS, "/action_1.txt");
+ }
+ else if (var == "action_2.txt")
+ {
+ return readFile(LITTLEFS, "/action_2.txt");
+ }
+ else if (var == "gp_1.txt")
+ {
+ return readFile(LITTLEFS, "/gp_2.txt");
+ }
+ else if (var == "gp_2.txt")
+ {
+ return readFile(LITTLEFS, "/gp_2.txt");
+ }
+
+ //////////////////
+ else if (var == "test_val_1")
+ {
+ return readFile(LITTLEFS, "/test_val_1");
+ }
+ else if (var == "test_val_2")
+ {
+ return readFile(LITTLEFS, "/test_val_2");
+ }
+
+ ////////////////
+
+ return String();
+}
+
+// ModbusMessage DATA;
+
+///////////////////
+
+char ssid[] = MY_SSID; // SSID and ...
+char pass[] = MY_PASS; // password for the WiFi network used
+
+// Create a ModbusTCP client instance
+ModbusClientTCPasync MB(Modbus_ip, Modbus_port);
+
+// Define an onData handler function to receive the regular responses
+// Arguments are Modbus server ID, the function code requested, the message data and length of it,
+// plus a user-supplied token to identify the causing request
+void handleData(ModbusMessage response, uint32_t token)
+{
+ // Serial.printf("Response: serverID=%d, FC=%d, Token=%08X, length=%d:\n", response.getServerID(), response.getFunctionCode(), token, response.size());
+ for (auto &byte : response)
+ {
+ // Serial.printf("%02X ", byte);
+ }
+ // debugln("");
+
+ switch (response.getServerID())
+ {
+ case 1:
+ response.get(3, Kostal_Pow_L1);
+ modbus_debug("Watt Kostal L1 : ");
+ modbus_debugln(Kostal_Pow_L1 / 1);
+ break;
+
+ case 2:
+ response.get(3, Kostal_Pow_L2);
+ modbus_debug("Watt Kostal L2 : ");
+ modbus_debugln(Kostal_Pow_L2 / 1);
+ break;
+
+ case 3:
+ response.get(3, Kostal_Pow_L3);
+ modbus_debug("Watt Kostal L3 : ");
+ modbus_debugln(Kostal_Pow_L3 / 1);
+ break;
+
+ case 4:
+ response.get(3, Kostal_Pow_tot);
+ modbus_debug("Watt Kostal total : ");
+ modbus_debugln(Kostal_Pow_tot / 1);
+ }
+
+ Kostal_Pow_L1_L2 = (Kostal_Pow_L1 + Kostal_Pow_L2) / 1;
+ modbus_debug("Watt Kostal L1_L2 : ");
+ modbus_debugln(Kostal_Pow_L1_L2);
+
+ Kostal_Pow_L2_L3 = (Kostal_Pow_L2 + Kostal_Pow_L3) / 1;
+ modbus_debug("Watt Kostal L2_L3 : ");
+ modbus_debugln(Kostal_Pow_L2_L3);
+
+ Kostal_Pow_L1_L3 = (Kostal_Pow_L1 + Kostal_Pow_L3) / 1;
+ modbus_debug("Watt Kostal L1_L3 : ");
+ modbus_debugln(Kostal_Pow_L1_L3);
+}
+
+// Define an onError handler function to receive error responses
+// Arguments are the error code returned and a user-supplied token to identify the causing request
+void handleError(Error error, uint32_t token)
+{
+ // ModbusError wraps the error code and provides a readable error message for it
+ ModbusError me(error);
+ Serial.printf("Error response: %02X - %s token: %d\n", (int)me, (const char *)me, token);
+}
+
+// Setup() - initialization happens here
+
+void subscriptions()
+{
+ MQTT_Client.subscribe(Subscription_will);
+ MQTT_Client.subscribe(Subscription_1);
+ MQTT_Client.subscribe(Subscription_2);
+ MQTT_Client.subscribe(Subscription_3);
+ MQTT_Client.subscribe(Subscription_4);
+ MQTT_Client.subscribe(Subscription_5);
+ /* MQTT_Client.subscribe(Subscription_6);
+ MQTT_Client.subscribe(Subscription_7);
+ MQTT_Client.subscribe(Subscription_8);
+ MQTT_Client.subscribe(Subscription_9);
+ MQTT_Client.subscribe(Subscription_10);
+ MQTT_Client.subscribe(Subscription_11);
+ */
+}
+
+void mqtt_reconnect()
+{ // Loop until reconnected
+ while (!MQTT_Client.connected())
+ {
+ debug("Attempting MQTT connection...");
+ if (MQTT_Client.connect(MQTT_CLIENT_ID))
+ { // Attempt to connect
+ debugln("connected");
+ MQTT_Client.publish(MQTT_OUT_TOPIC, "connected");
+ // MQTT_Client.subscribe(MQTT_IN_TOPIC); // ... and resubscribe
+
+ subscriptions();
+ }
+ else
+ {
+ debugln("failed, rc=" + String(MQTT_Client.state()) +
+ " try again in 5 seconds");
+ delay(5000); // Wait 5 seconds before retrying
+ }
+ }
+}
+
+void mqtt_callback(char *topic, byte *payload, unsigned int length)
+{
+
+ mqtt_debug("Message arrived in topic: ");
+ mqtt_debugln(topic);
+
+ mqtt_debug("Subscribe payload:");
+ for (int i = 0; i < length; i++)
+ {
+ mqtt_debug((char)payload[i]);
+ }
+
+ mqtt_debugln();
+ mqtt_debugln("-----------------------");
+
+ // Will force input to 0
+
+ if (strcmp(topic, Subscription_will) == 0)
+ {
+ if (msg != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ msg = 1;
+
+ var_Will = msg_p.toFloat(); // to float
+
+ if (var_Will == 0)
+ {
+ var_Einsp_tot = 0.0;
+ var_Einsp_L1 = 0.0;
+ var_Einsp_L2 = 0.0;
+ var_Einsp_L3 = 0.0;
+ }
+ }
+ }
+
+ if (strcmp(topic, Subscription_1) == 0)
+ {
+ if (msg != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ msg = 1;
+
+ var_Einsp_tot = msg_p.toFloat(); // to float
+ }
+ }
+ if (strcmp(topic, Subscription_2) == 0)
+ {
+ if (msg_L1 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_Einsp_L1 = msg_p.toFloat(); // to float
+ msg_L1 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_3) == 0)
+ {
+ if (msg_L2 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_Einsp_L2 = msg_p.toFloat(); // to float
+ msg_L2 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_4) == 0)
+ {
+ if (msg_L3 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_Einsp_L3 = msg_p.toFloat(); // to float
+ msg_L3 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_5) == 0)
+ {
+ if (msg_Exp != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ msg_Exp = 1;
+
+ var_Einsp_tot = msg_p.toFloat(); // to float
+ }
+ }
+ /*
+ if (strcmp(topic, Subscription_6) == 0)
+ {
+ if (msg_upper_1 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_upper_temp_1 = msg_p.toFloat(); // to float
+ msg_upper_1 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_7) == 0)
+ {
+ if (msg_lower_1 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_lower_temp_1 = msg_p.toFloat(); // to float
+ msg_lower_1 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_8) == 0)
+ {
+ if (msg_max_1 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_max_temp_1 = msg_p.toFloat(); // to float
+ msg_max_1 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_9) == 0)
+ {
+ if (msg_upper_2 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_upper_temp_2 = msg_p.toFloat(); // to float
+ msg_upper_2 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_10) == 0)
+ {
+ if (msg_lower_2 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_lower_temp_2 = msg_p.toFloat(); // to float
+ msg_lower_2 = 1;
+ }
+ }
+ if (strcmp(topic, Subscription_11) == 0)
+ {
+ if (msg_max_2 != 1)
+ {
+ char buff_p[length];
+ for (int i = 0; i < length; i++)
+ {
+ buff_p[i] = (char)payload[i];
+ }
+ buff_p[length] = '\0';
+ String msg_p = String(buff_p);
+ var_max_temp_2 = msg_p.toFloat(); // to float
+ msg_max_2 = 1;
+ }
+ }
+ */
+}
+
+void printDirectory(File dir, int numTabs = 3);
+
+void printDirectory(File dir, int numTabs)
+
+{
+ while (true)
+ {
+
+ File entry = dir.openNextFile();
+ if (!entry)
+ {
+ // no more files
+ break;
+ }
+ for (uint8_t i = 0; i < numTabs; i++)
+ {
+ debug('\t');
+ }
+ debug(entry.name());
+ if (entry.isDirectory())
+ {
+ debugln("/");
+ printDirectory(entry, numTabs + 1);
+ }
+ else
+ {
+ // files have sizes, directories do not
+ Serial.print("\t\t");
+ Serial.println(entry.size(), DEC);
+ }
+ entry.close();
+ }
+}
+
+void setup()
+
+ // Configures static IP address
+ if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
+ Serial.println("STA Failed to configure");
+ }
+
+{
+
+ // pinmode
+
+ pinMode(SSR_1, OUTPUT);
+ pinMode(SSR_2, OUTPUT);
+
+ /// PWM
+
+ ledcAttachPin(SSR_1, PWM_Channel_0);
+ ledcSetup(PWM_Channel_0, PWM_Freq, PWM_Resolution);
+
+ // Init Serial monitor
+
+ Serial.begin(115200);
+
+ while (!Serial)
+ {
+ }
+ debugln("__ OK __");
+
+ // stune
+
+ tuner.Configure(inputSpan, outputSpan, outputStart, outputStep, testTimeSec, settleTimeSec, samples);
+ tuner.SetEmergencyStop(tempLimit);
+
+ // Connect to WiFi
+ WiFi.begin(ssid, pass);
+ delay(200);
+ while (WiFi.status() != WL_CONNECTED)
+ {
+ debug(". ");
+ delay(1000);
+ }
+
+ debugln(F("Inizializing FS..."));
+ if (LITTLEFS.begin())
+ {
+ debugln(F("done."));
+ }
+ else
+ {
+ debugln(F("fail."));
+ }
+ debugln("File sistem info.");
+
+ debug("Total space: ");
+ debug(totalBytes);
+ debugln("byte");
+
+ debug("Total space used: ");
+ debug(usedBytes);
+ debugln("byte");
+
+ debug("Total space free: ");
+
+ debugln("byte");
+
+ debugln();
+
+ // Open dir folder
+ File dir = LITTLEFS.open("/");
+
+ // Cycle all the content
+ printDirectory(dir);
+
+ // init_wifi();
+ MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT);
+ MQTT_Client.setCallback(mqtt_callback);
+
+ ////////////
+ // Send web page with input fields to client
+
+ httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(LITTLEFS, "/www/index.html", "text/html", false); });
+
+ httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(LITTLEFS, "/www/index.html", "text/html", processor); });
+
+ // Problems here with serveStatic
+
+ // httpServer.serveStatic("/", LITTLEFS, "/").setDefaultFile("index.html");//not working see bug ???
+ // httpServer.serveStatic("/index1.html", LITTLEFS, "/index1.html");
+
+ // Send a GET request to /get?inputString=
+ httpServer.on("/get", HTTP_GET, [](AsyncWebServerRequest *request)
+ {
+ String inputMessage;
+ // GET inputString value on /get?inputString=
+
+ if (request->hasParam(PARAM_STRING)) {
+ inputMessage = request->getParam(PARAM_STRING)->value();
+ writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_INT)) {
+ inputMessage = request->getParam(PARAM_INT)->value();
+ writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_FLOAT)) {
+ inputMessage = request->getParam(PARAM_FLOAT)->value();
+ writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_INT_1)) {
+ inputMessage = request->getParam(PARAM_INT_1)->value();
+ writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_INT_2)) {
+ inputMessage = request->getParam(PARAM_INT_2)->value();
+ writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_INT_3)) {
+ inputMessage = request->getParam(PARAM_INT_3)->value();
+ writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_INT_4)) {
+ inputMessage = request->getParam(PARAM_INT_4)->value();
+ writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_Setpoint))
+ {
+ inputMessage = request->getParam(PARAM_Setpoint)->value();
+ writeFile(LITTLEFS, "/Setpoint.txt", inputMessage.c_str());
+ }
+
+
+ else if (request->hasParam(PARAM_Setpoint_1)) {
+ inputMessage = request->getParam(PARAM_Setpoint_1)->value();
+ writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_Setpoint_2)) {
+ inputMessage = request->getParam(PARAM_Setpoint_2)->value();
+ writeFile(LITTLEFS, "/Setpoint_2.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_Kp)) {
+ inputMessage = request->getParam(PARAM_Kp)->value();
+ writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str());
+ }
+
+
+ else if (request->hasParam(PARAM_Ki)) {
+ inputMessage = request->getParam(PARAM_Ki)->value();
+ writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_Kd)) {
+ inputMessage = request->getParam(PARAM_Kd)->value();
+ writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_aggKp)) {
+ inputMessage = request->getParam(PARAM_aggKp)->value();
+ writeFile(LITTLEFS, "/aggKp.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_aggKi)) {
+ inputMessage = request->getParam(PARAM_aggKi)->value();
+ writeFile(LITTLEFS, "/aggKi.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_aggKd)) {
+ inputMessage = request->getParam(PARAM_aggKd)->value();
+ writeFile(LITTLEFS, "/aggKd.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_Kp_1)) {
+ inputMessage = request->getParam(PARAM_Kp_1)->value();
+ writeFile(LITTLEFS, "/Kp_1.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_Ki_1)) {
+ inputMessage = request->getParam(PARAM_Ki_1)->value();
+ writeFile(LITTLEFS, "/Ki_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_Kd_1)) {
+ inputMessage = request->getParam(PARAM_Kd_1)->value();
+ writeFile(LITTLEFS, "/Kd_1.txt", inputMessage.c_str());
+ }
+ //////////
+ else if(request->hasParam(PARAM_aggKp_1)) {
+ inputMessage = request->getParam(PARAM_aggKp_1)->value();
+ writeFile(LITTLEFS, "/aggKp_1.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_aggKi_1)) {
+ inputMessage = request->getParam(PARAM_aggKi_1)->value();
+ writeFile(LITTLEFS, "/aggKi_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_aggKd_1)) {
+ inputMessage = request->getParam(PARAM_aggKd_1)->value();
+ writeFile(LITTLEFS, "/aggKd_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_Kp_2)) {
+ inputMessage = request->getParam(PARAM_Kp_2)->value();
+ writeFile(LITTLEFS, "/Kp_2.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_Ki_2)) {
+ inputMessage = request->getParam(PARAM_Ki_2)->value();
+ writeFile(LITTLEFS, "/Ki_2.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_Kd_2)) {
+ inputMessage = request->getParam(PARAM_Kd_2)->value();
+ writeFile(LITTLEFS, "/Kd_2.txt", inputMessage.c_str());
+ }
+
+
+
+ else if(request->hasParam(PARAM_aggKp_2)) {
+ inputMessage = request->getParam(PARAM_aggKp_2)->value();
+ writeFile(LITTLEFS, "/aggKp_2.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_aggKi_2)) {
+ inputMessage = request->getParam(PARAM_aggKi_2)->value();
+ writeFile(LITTLEFS, "/aggKi_2.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_aggKd_2)) {
+ inputMessage = request->getParam(PARAM_aggKd_2)->value();
+ writeFile(LITTLEFS, "/aggKd_2.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_select_Input_SSR_1)) {
+ inputMessage = request->getParam(PARAM_select_Input_SSR_1)->value();
+ writeFile(LITTLEFS, "/select_Input_SSR_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_select_Input_SSR_2)) {
+ inputMessage = request->getParam(PARAM_select_Input_SSR_2)->value();
+ writeFile(LITTLEFS, "/select_Input_SSR_2.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_SSR_1_setpoint_distance)) {
+ inputMessage = request->getParam(PARAM_SSR_1_setpoint_distance)->value();
+ writeFile(LITTLEFS, "/SSR_1_setpoint_distance.txt", inputMessage.c_str());
+ }
+
+
+
+ else if(request->hasParam(PARAM_SSR_1_PID_direction)) {
+ inputMessage = request->getParam(PARAM_SSR_1_PID_direction)->value();
+ writeFile(LITTLEFS, "/SSR_1_PID_direction.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_SSR_1_on)) {
+ inputMessage = request->getParam(PARAM_SSR_1_on)->value();
+ writeFile(LITTLEFS, "/SSR_1_on.txt", inputMessage.c_str());
+
+ }
+ else if(request->hasParam(PARAM_SSR_1_off)) {
+ inputMessage = request->getParam(PARAM_SSR_1_off)->value();
+ writeFile(LITTLEFS, "/SSR_1_off.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_SSR_2_on)) {
+ inputMessage = request->getParam(PARAM_SSR_2_on)->value();
+ writeFile(LITTLEFS, "/SSR_2_on.txt", inputMessage.c_str());
+
+ }
+ else if(request->hasParam(PARAM_SSR_2_off)) {
+ inputMessage = request->getParam(PARAM_SSR_2_off)->value();
+ writeFile(LITTLEFS, "/SSR_2_off.txt", inputMessage.c_str());
+ }
+
+
+
+
+ else if(request->hasParam(PARAM_PWM_Freq)) {
+ inputMessage = request->getParam(PARAM_PWM_Freq)->value();
+ writeFile(LITTLEFS, "/PWM_Freq.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_PWM_Resolution)) {
+ inputMessage = request->getParam(PARAM_PWM_Resolution)->value();
+ writeFile(LITTLEFS, "/PWM_Resolution.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_SSR_1_method)) {
+ inputMessage = request->getParam(PARAM_SSR_1_method)->value();
+ writeFile(LITTLEFS, "/SSR_1_method.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_SSR_2_method)) {
+ inputMessage = request->getParam(PARAM_SSR_2_method)->value();
+ writeFile(LITTLEFS, "/SSR_2_method.txt", inputMessage.c_str());
+ }
+
+
+
+ else if(request->hasParam(PARAM_SSR_1_mode)) {
+ inputMessage = request->getParam(PARAM_SSR_1_mode)->value();
+ writeFile(LITTLEFS, "/SSR_1_mode.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_SSR_2_mode)) {
+ inputMessage = request->getParam(PARAM_SSR_2_mode)->value();
+ writeFile(LITTLEFS, "/SSR_2_mode.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_min_range_1)) {
+ inputMessage = request->getParam(PARAM_min_range_1)->value();
+ writeFile(LITTLEFS, "/min_range_1.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_min_range_2)) {
+ inputMessage = request->getParam(PARAM_min_range_2)->value();
+ writeFile(LITTLEFS, "/min_range_2.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_max_range_1)) {
+ inputMessage = request->getParam(PARAM_max_range_1)->value();
+ writeFile(LITTLEFS, "/max_range_1.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_max_range_2)) {
+ inputMessage = request->getParam(PARAM_max_range_2)->value();
+ writeFile(LITTLEFS, "/max_range_2.txt", inputMessage.c_str());
+
+ }
+
+ else if(request->hasParam(PARAM_upper_1)) {
+ inputMessage = request->getParam(PARAM_upper_1)->value();
+ writeFile(LITTLEFS, "/upper_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_lower_1)) {
+ inputMessage = request->getParam(PARAM_lower_1)->value();
+ writeFile(LITTLEFS, "/lower_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_max_1)) {
+ inputMessage = request->getParam(PARAM_max_1)->value();
+ writeFile(LITTLEFS, "/max_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_upper_2)) {
+ inputMessage = request->getParam(PARAM_upper_2)->value();
+ writeFile(LITTLEFS, "/upper_2.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_lower_2)) {
+ inputMessage = request->getParam(PARAM_lower_2)->value();
+ writeFile(LITTLEFS, "/lower_2.txt", inputMessage.c_str());
+ }
+ else if(request->hasParam(PARAM_max_2)) {
+ inputMessage = request->getParam(PARAM_max_2)->value();
+ writeFile(LITTLEFS, "/max_2.txt", inputMessage.c_str());
+ }
+
+
+ else if(request->hasParam(PARAM_test_val_1)) {
+ inputMessage = request->getParam(PARAM_test_val_1)->value();
+ writeFile(LITTLEFS, "/test_val_1.txt", inputMessage.c_str());
+ }
+
+ else if(request->hasParam(PARAM_test_val_2)) {
+ inputMessage = request->getParam(PARAM_test_val_2)->value();
+ writeFile(LITTLEFS, "/test_val_2.txt", inputMessage.c_str());
+ }
+ else {
+ inputMessage = "No message sent";
+ }
+ debugln(inputMessage);
+ request->send(200, "text/text", inputMessage); });
+
+ httpServer.onNotFound(notFound);
+ httpServer.begin();
+
+ IPAddress wIP = WiFi.localIP();
+ Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]);
+
+ // Set up ModbusTCP client.
+ // - provide onData handler function
+ MB.onDataHandler(&handleData);
+ // - provide onError handler function
+ MB.onErrorHandler(&handleError);
+ // Set message timeout to 2000ms and interval between requests to the same host to 200ms
+ MB.setTimeout(2000);
+ // Start ModbusTCP background task
+ MB.setIdleTimeout(10000);
+ MB.setMaxInflightRequests(maxInflightRequests);
+
+ //// OTA
+
+ // ArduinoOTA.begin();
+
+ // DFPlayer
+}
+
+// End Setup
+
+// loop() - nothing done here today!
+void loop()
+{
+ ArduinoOTA.handle(); // fuer Flashen ueber WLAN
+ if (Modbus_on_off == 1) // Modbus ein und Aus schalten
+ {
+ static unsigned long lastMillis = 0;
+ if (millis() - lastMillis > 5000)
+ {
+ lastMillis = millis();
+
+ // Create request for
+ // (Fill in your data here!)
+ // - serverID = 1
+ // - function code = 0x03 (read holding register)
+ // - start address to read = word 40084
+ // - number of words to read = 1
+ // - token to match the response with the request. We take the current millis() value for it.
+ //
+ // If something is missing or wrong with the call parameters, we will immediately get an error code
+ // and the request will not be issued
+ Serial.printf("sending request with token %d\n", (uint32_t)lastMillis);
+ Error error;
+ error = MB.addRequest((uint32_t)lastMillis, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_1, 1); // Abfrage 1
+ error = MB.addRequest((uint32_t)lastMillis + 1, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_2, 1); // Abfrage 2
+ error = MB.addRequest((uint32_t)lastMillis + 2, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_3, 1); // Abfrage 3
+ error = MB.addRequest((uint32_t)lastMillis + 3, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_4, 1); // Abfrage 4
+ if (error != SUCCESS)
+ {
+ ModbusError e(error);
+ Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e);
+ }
+
+ // Else the request is processed in the background task and the onData/onError handler functions will get the result.
+ //
+ // The output on the Serial Monitor will be (depending on your WiFi and Modbus the data will be different):
+ // __ OK __
+ // . WIFi IP address: 192.168.178.74
+ // Response: serverID=20, FC=3, Token=0000056C, length=11:
+ // 14 03 04 01 F6 FF FF FF 00 C0 A8
+ }
+ }
+ if (!MQTT_Client.connected())
+ {
+ mqtt_reconnect();
+ }
+
+ MQTT_Client.loop();
+
+ if (msg == 1)
+ { // check if new callback message
+
+ float_Voltaik_Surplus_tot = var_Einsp_tot; // to float
+ // int_Voltaik_Surplus_tot = msg_t.toInt(); // to Int
+ mqtt_debug("Excess Solar total: ");
+ mqtt_debugln(float_Voltaik_Surplus_tot);
+ mqtt_debugln("-----------------------");
+
+ msg = 0; // reset message flag
+
+ if (msg_L1 == 1)
+ {
+ float_Voltaik_Surplus_L1 = var_Einsp_L1;
+ mqtt_debug("Excess Solar L1: ");
+ mqtt_debugln(float_Voltaik_Surplus_L1);
+ mqtt_debugln("-----------------------");
+ msg_L1 = 0;
+ }
+
+ if (msg_L2 == 1)
+ {
+ float_Voltaik_Surplus_L2 = var_Einsp_L2;
+ mqtt_debug("Excess L2: ");
+ mqtt_debugln(float_Voltaik_Surplus_L2);
+ mqtt_debugln("-----------------------");
+ msg_L2 = 0;
+ }
+
+ if (msg_L3 == 1)
+ {
+ float_Voltaik_Surplus_L3 = var_Einsp_L3;
+ mqtt_debug("Excess L3: ");
+ mqtt_debugln(float_Voltaik_Surplus_L3);
+ mqtt_debugln("-----------------------");
+ msg_L3 = 0;
+ }
+ }
+
+ //-----------------------SSR1-------------------------//
+
+ select_Input_SSR_1 = readFile(LITTLEFS, "/select_Input_SSR_1.txt").toFloat();
+
+ test_val_1 = readFile(LITTLEFS, "/test_val_1.txt").toFloat();
+ test_val_2 = readFile(LITTLEFS, "/test_val_2.txt").toFloat();
+
+ switch (select_Input_SSR_1)
+ {
+ case 0:
+ ssr_debugln("input_1 case 0 ");
+ val_Excess_SSR_1 = 0;
+ ssr_debug("SSR 1 no value: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 1:
+ ssr_debugln("input_1 case 1 ");
+ val_Excess_SSR_1 = float_Voltaik_Surplus_tot;
+ ssr_debug("SSR 1 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 2:
+ ssr_debugln("input_1 case 2 ");
+ val_Excess_SSR_1 = float_Voltaik_Surplus_L1;
+ ssr_debug("SSR 1 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 3:
+ ssr_debugln("input_1 case 3 ");
+ val_Excess_SSR_1 = float_Voltaik_Surplus_L2;
+ ssr_debug("SSR 1 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 4:
+ ssr_debugln("input_1 case 4 ");
+ val_Excess_SSR_1 = float_Voltaik_Surplus_L3;
+ ssr_debug("SSR 1 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 5:
+ ssr_debugln("input_1 case 5 ");
+ val_Excess_SSR_1 = Kostal_Pow_L1;
+ ssr_debug("SSR 1 Inverter L1: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 6:
+ ssr_debugln("input_1 case 6 ");
+ val_Excess_SSR_1 = Kostal_Pow_L2;
+ ssr_debug("SSR 1 Inverter L2: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 7:
+ ssr_debugln("input_1 case 7 ");
+ val_Excess_SSR_1 = Kostal_Pow_L3;
+ ssr_debug("SSR 1 Inverter L3: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 8:
+ ssr_debugln("input_1 case 8 ");
+ val_Excess_SSR_1 = Kostal_Pow_tot;
+ ssr_debug("SSR 1 Inverter L1 L2 L3: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 9:
+ ssr_debugln("input_1 case 9 ");
+ val_Excess_SSR_1 = Kostal_Pow_L1_L2;
+ ssr_debug("SSR 1 Inverter L1 L2 : ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 10:
+ ssr_debugln("input_1 case 10 ");
+ val_Excess_SSR_1 = Kostal_Pow_L1_L3;
+ ssr_debug("SSR 1 Inverter L1 L3: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 11:
+ ssr_debugln("input_1 case 11 ");
+ val_Excess_SSR_1 = Kostal_Pow_L2_L3;
+ ssr_debug("SSR 1 Inverter L2 L3: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 12:
+ ssr_debugln("input_1 case 12 ");
+ val_Excess_SSR_1 = test_val_1;
+ ssr_debug("SSR 1 test_val_1: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ case 13:
+ ssr_debugln("input_1 case 13 ");
+ val_Excess_SSR_1 = test_val_2;
+ ssr_debug("SSR 1 test_val_2: ");
+ ssr_debugln(val_Excess_SSR_1);
+ ssr_debugln("-----------------------");
+ break;
+ }
+ ///////////////////////////////
+ //-----------------------SSR2-------------------------//
+
+ select_Input_SSR_2 = readFile(LITTLEFS, "/select_Input_SSR_2.txt").toFloat();
+ test_val_1 = readFile(LITTLEFS, "/test_val_1.txt").toFloat();
+ test_val_2 = readFile(LITTLEFS, "/test_val_2.txt").toFloat();
+
+ switch (select_Input_SSR_2)
+ {
+ case 0:
+ ssr_debugln("input_1 case 0 ");
+ val_Excess_SSR_2 = 0;
+ ssr_debug("SSR 2 no value: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 1:
+ ssr_debugln("input_1 case 1 ");
+ val_Excess_SSR_2 = float_Voltaik_Surplus_tot;
+ ssr_debug("SSR 2 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 2:
+ ssr_debugln("input_1 case 2 ");
+ val_Excess_SSR_2 = float_Voltaik_Surplus_L1;
+ ssr_debug("SSR 2 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 3:
+ ssr_debugln("input_1 case 3 ");
+ val_Excess_SSR_2 = float_Voltaik_Surplus_L2;
+ ssr_debug("SSR 2 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 4:
+ ssr_debugln("input_1 case 4 ");
+ val_Excess_SSR_2 = float_Voltaik_Surplus_L3;
+ ssr_debug("SSR 2 Excess Solar total: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 5:
+ ssr_debugln("input_1 case 5 ");
+ val_Excess_SSR_2 = Kostal_Pow_L1;
+ ssr_debug("SSR 2 Inverter L1: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 6:
+ ssr_debugln("input_1 case 6 ");
+ val_Excess_SSR_2 = Kostal_Pow_L2;
+ ssr_debug("SSR 2 Inverter L2: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 7:
+ ssr_debugln("input_1 case 7 ");
+ val_Excess_SSR_2 = Kostal_Pow_L3;
+ ssr_debug("SSR 2 Inverter L3: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 8:
+ ssr_debugln("input_1 case 8 ");
+ val_Excess_SSR_2 = Kostal_Pow_tot;
+ ssr_debug("SSR 2 Inverter L1 L2 L3: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 9:
+ ssr_debugln("input_1 case 9 ");
+ val_Excess_SSR_2 = Kostal_Pow_L1_L2;
+ ssr_debug("SSR 2 Inverter L1 L2 : ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 10:
+ ssr_debugln("input_1 case 10 ");
+ val_Excess_SSR_2 = Kostal_Pow_L1_L3;
+ ssr_debug("SSR 2 Inverter L1 L3: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 11:
+ ssr_debugln("input_1 case 11 ");
+ val_Excess_SSR_2 = Kostal_Pow_L2_L3;
+ ssr_debug("SSR 2 Inverter L2 L3: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 12:
+ ssr_debugln("input_1 case 12 ");
+ val_Excess_SSR_2 = test_val_1;
+
+ ssr_debug("SSR 2 test_val_1: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ case 13:
+ ssr_debugln("input_1 case 13 ");
+ val_Excess_SSR_2 = test_val_2;
+ ssr_debug("SSR 2 test_val_2: ");
+ ssr_debugln(val_Excess_SSR_2);
+ ssr_debugln("-----------------------");
+ break;
+ }
+ ///////////////////////////////
+
+ //////////////////////////// SSR1 ///////////////////////
+
+ SSR_1_on = readFile(LITTLEFS, "/SSR_1_on.txt").toFloat();
+ SSR_1_off = readFile(LITTLEFS, "/SSR_1_off.txt").toFloat();
+ SSR_1_mode = readFile(LITTLEFS, "/SSR_1_mode.txt").toFloat();
+ PWM_Resolution = readFile(LITTLEFS, "/PWM_Resolution.txt").toFloat();
+
+ if (SSR_1_mode == 0)
+ {
+ ledcSetup(PWM_Channel_0, PWM_Freq, 12);
+
+ ssr_debugln("------------- SSR_1 - mode 0 ---------");
+
+ if (val_Excess_SSR_1 < SSR_1_off)
+ {
+ Var_PWM_1 = 0;
+ ssr_debugln("------------- SSR_1 ----------");
+ ssr_debug("SSR 1 is switched on at value :");
+ ssr_debugln(SSR_1_on);
+ ssr_debug("SSR 1 is switched off at value : ");
+ ssr_debugln(SSR_1_off);
+ ssr_debugln("-----------------------");
+ ssr_debugln("SSR 1 is off :");
+ ssr_debug("value SSR_1 is : ");
+ ssr_debugln(val_Excess_SSR_1);
+ }
+
+ if (val_Excess_SSR_1 > SSR_1_on)
+ {
+ Var_PWM_1 = 4095;
+
+ ssr_debugln("-----------------------");
+ ssr_debug("SSR 1 is switched on at value :");
+ ssr_debugln(SSR_1_on);
+ ssr_debug("SSR 1 is switched off at value : ");
+ ssr_debugln(SSR_1_off);
+ ssr_debugln("-----------------------");
+ ssr_debugln("SSR 1 is on :");
+ ssr_debug("val_Excess_SSR_1 is : ");
+ ssr_debugln(val_Excess_SSR_1);
+ }
+ ledcWrite(PWM_Channel_0, Var_PWM_1);
+ }
+ if (SSR_1_mode == 1)
+ {
+ ledcSetup(PWM_Channel_0, PWM_Freq, 12);
+
+ if ((val_Excess_SSR_1 >= SSR_1_off) && (val_Excess_SSR_1 <= SSR_1_on))
+ {
+ Var_PWM_1 = map(val_Excess_SSR_1, SSR_1_off, SSR_1_on, 0, 4095);
+ }
+ if (val_Excess_SSR_1 < SSR_1_off)
+ {
+ Var_PWM_1 = 0;
+ }
+ if (val_Excess_SSR_1 > SSR_1_on)
+ {
+ Var_PWM_1 = 4095;
+ }
+
+ ledcWrite(PWM_Channel_0, Var_PWM_1);
+ pwm_debugln("SSR 1 is switched to mapped PWM mode : ");
+ pwm_debug("SSR 1 PWM is : ");
+ pwm_debugln(Var_PWM_1);
+ pwm_debug("SSR 1 PWM Frequency : ");
+ pwm_debugln(PWM_Freq);
+ pwm_debug("SSR 1 PWM Resolution : ");
+ pwm_debugln(PWM_Resolution);
+ }
+
+ //////////Mode2///////
+
+ if (SSR_1_mode == 2)
+ {
+ ledcSetup(PWM_Channel_0, PWM_Freq, PWM_Resolution);
+ // PID 2
+ pid_debugln("-------------");
+ pid_debugln("SSR 1 is switched to PID PWM mede: ");
+ SSR_1_PID_direction = readFile(LITTLEFS, "/SSR_1_PID_direction.txt").toFloat();
+ PWM_Resolution = readFile(LITTLEFS, "/PWM_Resolution.txt").toFloat();
+
+ switch (PWM_Resolution)
+ {
+ case 4:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 4 bit");
+ PID_max = 15;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 5:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 5 bit");
+ PID_max = 31;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 6:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 6 bit");
+ PID_max = 63;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 7:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 7 bit");
+ PID_max = 127;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 8:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 8 bit");
+ PID_max = 255;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 9:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 9 bit");
+ PID_max = 511;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+ case 10:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 10 bit");
+ PID_max = 1023;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+ case 11:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 11 bit");
+ PID_max = 2047;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+ case 12:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 12 bit");
+ PID_max = 4095;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+ case 13:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 13 bit");
+ PID_max = 8191;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 14:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 14 bit");
+ PID_max = 16383;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 15:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 15 bit");
+ PID_max = 32767;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+
+ case 16:
+ pid_debugln("-------------");
+ pid_debugln("PID Resolution 16 bit");
+ PID_max = 65535;
+ pid_debug("PID Max Steps : 0 - ");
+ pid_debugln(PID_max);
+ break;
+ }
+
+ // turn the PID on
+ myPID_4.SetMode(myPID.Control::automatic);
+ myPID_4.SetOutputLimits(PID_min, PID_max);
+
+ myPID_4.SetSampleTimeUs(SampleTimeUs);
+ if (SSR_1_PID_direction == 1)
+ {
+ myPID_4.SetControllerDirection(myPID_4.Action::reverse);
+ pid_debugln("PID direction <<---- reverse");
+ }
+ else
+ {
+ myPID_4.SetControllerDirection(myPID_4.Action::direct);
+ pid_debugln("PID direction ----->> direct");
+ }
+
+ Input_4 = val_Excess_SSR_1;
+ Setpoint_4 = readFile(LITTLEFS, "/Setpoint.txt").toFloat();
+ SSR_1_setpoint_distance = readFile(LITTLEFS, "/SSR_1_setpoint_distance.txt").toFloat();
+ consKp = readFile(LITTLEFS, "/Kp.txt").toFloat();
+ consKi = readFile(LITTLEFS, "/Ki.txt").toFloat();
+ consKd = readFile(LITTLEFS, "/Kd.txt").toFloat();
+ aggKp = readFile(LITTLEFS, "/aggKp.txt").toFloat();
+ aggKi = readFile(LITTLEFS, "/aggKi.txt").toFloat();
+ aggKd = readFile(LITTLEFS, "/aggKd.txt").toFloat();
+ float gap_4 = abs(Setpoint_4 - Input_4); // distance away from setpoint
+ if (gap_4 < SSR_1_setpoint_distance)
+ { // we're close to setpoint, use conservative tuning parameters
+ myPID_4.SetTunings(consKp, consKi, consKd);
+ }
+ else
+ {
+ // we're far from setpoint, use aggressive tuning parameters
+ myPID_4.SetTunings(aggKp, aggKi, aggKd);
+ }
+
+ myPID_4.Compute();
+
+ pid_debugln("-------------");
+ pid_debug("Setpoint : ");
+ pid_debugln(Setpoint_4);
+ pid_debug("input: ");
+ pid_debugln(Input_4);
+ pid_debug("Setpoint distance: ");
+ pid_debugln(SSR_1_setpoint_distance);
+ pid_debug("gap: ");
+ pid_debugln(gap_4);
+ pid_debugln("-------------");
+ pid_debug("SSR 1 PWM output: ");
+ pid_debugln(Output_4);
+ pid_debugln("-------------");
+ pid_debug("consKp: ");
+ pid_debugln(consKp);
+ pid_debug("consKi: ");
+ pid_debugln(consKi);
+ pid_debug("consKd: ");
+ pid_debugln(consKd);
+ pid_debugln("-------------");
+ pid_debug("aggKp: ");
+ pid_debugln(aggKp);
+ pid_debug("aggKi: ");
+ pid_debugln(aggKi);
+ pid_debug("aggcKd: ");
+ pid_debugln(aggKd);
+ pid_debugln("-------------");
+
+ ledcWrite(PWM_Channel_0, Output_4);
+ ssr_debugln("SSR 1 is switched to PID PWM : ");
+ ssr_debug("SSR 1 PWM is : ");
+ ssr_debugln(Output_4);
+ }
+
+ if (SSR_1_mode > 2)
+ {
+ ssr_debug("SSR 1 mode incompatible out of range only 0, 1, 2: ");
+ }
+
+ //////////////////////////// SSR2 ///////////////////////
+ SSR_2_on = readFile(LITTLEFS, "/SSR_2_on.txt").toFloat();
+ SSR_2_off = readFile(LITTLEFS, "/SSR_2_off.txt").toFloat();
+ SSR_2_mode = readFile(LITTLEFS, "/SSR_2_mode.txt").toFloat();
+
+ if (SSR_2_mode == 0)
+ {
+ if (val_Excess_SSR_2 < SSR_2_off)
+ {
+ digitalWrite(SSR_2, LOW);
+ ssr_debugln("-----------------------");
+ ssr_debug("SSR 2 is switched on at value :");
+ ssr_debugln(SSR_2_on);
+ ssr_debug("SSR 2 is switched off at value : ");
+ ssr_debugln(SSR_2_off);
+ ssr_debugln("-----------------------");
+ ssr_debugln("SSR 2 is off :");
+ ssr_debug("value SSR_2 is : ");
+ ssr_debugln(val_Excess_SSR_2);
+ }
+ if (val_Excess_SSR_2 > SSR_2_on)
+ {
+ digitalWrite(SSR_2, HIGH);
+ ssr_debugln("-----------------------");
+ ssr_debug("SSR 2 is switched on at value :");
+ ssr_debugln(SSR_2_on);
+ ssr_debug("SSR 2 is switched off at value : ");
+ ssr_debugln(SSR_2_off);
+ ssr_debugln("-----------------------");
+ ssr_debugln("SSR 2 is on :");
+ ssr_debug("val_Excess_SSR_2 is : ");
+ ssr_debugln(val_Excess_SSR_2);
+ }
+ }
+ if (SSR_2_mode == 1)
+ {
+ ssr_debug("SSR 2 mode PWM Mapped is not implemented : ");
+ }
+ if (SSR_2_mode == 2)
+ {
+ ssr_debug("SSR 2 mode PWM PID is not implemented : ");
+ }
+ if (SSR_2_mode > 2)
+ {
+ ssr_debug("SSR 2 mode incompatible out of range only 0, 1, 2: ");
+ }
+
+ if (DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (DEBUGf == 1)
+ {
+ delay(5000);
+ }
+ else if (MQTT_DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (SMARTY_DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (MODBUS_DEBUG == 1)
+ {
+ delay(5000);
+ }
+
+ else if (PID_DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (SSR_DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (PWM_DEBUG == 1)
+ {
+ delay(5000);
+ }
+ else if (I2C_DEBUG == 1)
+ {
+ delay(5000);
+ }
+
+ else
+ {
+ }
+}
+
+// end loop
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt b/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt
new file mode 100644
index 0000000..3988a5b
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt
@@ -0,0 +1,924 @@
+
+/*
+LITTLEFSimpl::exists() bug with Platformio (ESP32 arduino framework) in Visual Studio Code
+Fix by adding third argument
+File f = open(path, "r", false);
+
+*/
+
+
+
+// Includes: for Serial etc., WiFi.h for WiFi support
+#include
+#include
+
+// for Smarty
+#include
+#include
+
+// for modbus Invrters read power
+
+#include
+
+#include
+#include
+#include
+
+/*
+#define USE_LittleFS
+
+#include
+
+#ifdef USE_LittleFS
+#define SPIFFS LITTLEFS
+#include
+#else
+#include
+#endif
+*/
+// PID
+#include
+#include
+//#include
+
+// Include the header for the ModbusClient TCP style
+#include
+
+#include "credentials.h"
+
+//#define BUILTIN_LED 2
+#define Alarm 27
+#define SSR_1 13
+#define SSR_2 14
+#define DAC1 25 // Identify the digital to analog converter pin
+#define DAC2 26 // Identify the digital to analog converter pin
+
+///////////////////
+
+//#include
+
+#include "special_settings.h"
+// switch on (1)or of (0) serial.print = serial.print replaced by debug //
+//#define DEBUG 1 in special special_settings.h
+
+#if DEBUG == 1
+#define debug(x) Serial.print(x)
+#define debugln(x) Serial.println(x)
+#else
+#define debug(x)
+#define debugln(x)
+#endif
+
+////////////////////
+
+uint16_t DS3_Pow_L1;
+uint16_t DS3_Pow_L2;
+uint16_t DS3_Pow_L3;
+uint16_t DS3_Pow_tot;
+
+uint16_t DS3_Pow_L1_L2;
+uint16_t DS3_Pow_L1_L3;
+uint16_t DS3_Pow_L2_L3;
+
+uint16_t Basic_load_L1;
+uint16_t Basic_load_L2;
+uint16_t Basic_load_L3;
+
+uint16_t Basic_load_tot;
+
+uint16_t Voltaik_Surplus_L1;
+uint16_t Voltaik_Surplus_L2;
+uint16_t Voltaik_Surplus_L3;
+
+uint16_t Voltaik_Surplus_tot;
+
+uint32_t maxInflightRequests = 1;
+// FĂĽr Smarty
+float var_Bezug_tot = 0;
+float var_Einsp_tot = 0;
+float var_Einsp_L1 = 0;
+float var_Einsp_L2 = 0;
+float var_Einsp_L3 = 0;
+
+float val_Bezug_tot = 0;
+float val_Einsp_tot = 0;
+float val_Einsp_L1 = 0;
+float val_Einsp_L2 = 0;
+float val_Einsp_L3 = 0;
+
+int val_Surplus = 0;
+int val_inverter = 0;
+
+// map range
+
+int min_range1 = 10;
+int max_range1 = 20;
+
+int min_range2 = 10;
+int max_range2 = 20;
+
+int val_DAC_1 = 0;
+int val_DAC_2 = 0;
+// PID
+
+float Setpoint_1;
+float aggKp_1;
+float aggKi_1;
+float aggKd_1;
+float consKp_1;
+float consKi_1;
+float consKd_1;
+float Input_1;
+float Output_1;
+
+float Setpoint_2;
+float aggKp_2;
+float aggKi_2;
+float aggKd_2;
+float consKp_2;
+float consKi_2;
+float consKd_2;
+float Input_2;
+float Output_2;
+
+QuickPID myPID_1(&Input_1, &Output_1, &Setpoint_1);
+QuickPID myPID_2(&Input_2, &Output_2, &Setpoint_2);
+
+// int select_Input_1 ;
+// int select_Input_2 ;
+
+// webpage Input
+
+const char* PARAM_INPUT_1 = "input_1";
+const char* PARAM_INPUT_2 = "input_2";
+const char *PARAM_INPUT_3 = "input_3";
+const char *PARAM_INPUT_4 = "input_4";
+
+const char *PARAM_INT_1 = "inputInt_1";
+const char *PARAM_INT_2 = "inputInt_2";
+const char *PARAM_INT_3 = "inputInt_3";
+const char *PARAM_INT_4 = "inputInt_4";
+
+const char *PARAM_Setpoint_1 = "Setpoint_1";
+const char *PARAM_Kp = "Kp";
+const char *PARAM_Ki = "Ki";
+const char *PARAM_Kd = "Kd";
+
+const char *PARAM_STRING = "inputString";
+const char *PARAM_INT = "inputInt";
+const char *PARAM_FLOAT = "inputFloat";
+// Modbus on off
+int MB_on_off = 1;
+
+// MQTT settings see credentials.h
+
+// Smarty subscriptions see specialsettings.h
+
+WiFiClient ESP32_Client;
+PubSubClient MQTT_Client(ESP32_Client);
+StaticJsonDocument<64> doc;
+
+int msg = 0;
+int msg_L1 = 0;
+int msg_L2 = 0;
+int msg_L3 = 0;
+
+// LITTLEFS
+unsigned int totalBytes = LITTLEFS.totalBytes();
+unsigned int usedBytes = LITTLEFS.usedBytes();
+
+// Webserver for parameter data input
+
+AsyncWebServer server(80);
+
+void notFound(AsyncWebServerRequest *request)
+{
+ request->send(404, "text/plain", "Not found");
+}
+
+String readFile(fs::FS &fs, const char *path)
+{
+ // Serial.printf("Reading file: %s\r\n", path);
+ File file = fs.open(path, "r");
+ if (!file || file.isDirectory())
+ {
+ debugln("- empty file or failed to open file");
+ return String();
+ }
+ // debugln("- read from file:");
+ String fileContent;
+ while (file.available())
+ {
+ fileContent += String((char)file.read());
+ }
+ file.close();
+ // debugln(fileContent);
+ return fileContent;
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message)
+{
+ Serial.printf("Writing file: %s\r\n", path);
+ File file = fs.open(path, "w");
+ if (!file)
+ {
+ debugln("- failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ debugln("- file written");
+ }
+ else
+ {
+ debugln("- write failed");
+ }
+ file.close();
+}
+
+// Replaces placeholder with stored values
+
+String processor(const String &var)
+{
+ debugln(var);
+ Serial.print("var");
+ Serial.println(var);
+ if (var == "inputString")
+ {
+ return readFile(LITTLEFS, "/inputString.txt");
+ }
+ else if (var == "inputInt")
+ {
+ return readFile(LITTLEFS, "/inputInt.txt");
+ }
+ else if (var == "inputFloat")
+ {
+ return readFile(LITTLEFS, "/inputFloat.txt");
+ }
+ else if (var == "inputInt_1")
+ {
+ return readFile(LITTLEFS, "/inputInt_1.txt");
+ }
+ else if (var == "inputInt_2")
+ {
+ return readFile(LITTLEFS, "/inputInt_2.txt");
+ }
+ else if (var == "inputInt_3")
+ {
+ return readFile(LITTLEFS, "/inputInt_3.txt");
+ }
+ else if (var == "inputInt_4")
+ {
+ return readFile(LITTLEFS, "/inputInt_4.txt");
+ }
+ else if (var == "Setpoint_1")
+ {
+ return readFile(LITTLEFS, "/Setpoint_1.txt");
+ }
+ else if (var == "Kp")
+ {
+ return readFile(LITTLEFS, "/Kp.txt");
+ }
+ else if (var == "Ki")
+ {
+ return readFile(LITTLEFS, "/Ki.txt");
+ }
+ else if (var == "Kd")
+ {
+ return readFile(LITTLEFS, "/Kd.txt");
+ }
+ return String();
+}
+
+// ModbusMessage DATA;
+
+///////////////////
+
+char ssid[] = MY_SSID; // SSID and ...
+char pass[] = MY_PASS; // password for the WiFi network used
+
+// Create a ModbusTCP client instance
+ModbusClientTCPasync MB(ip, port);
+
+// Define an onData handler function to receive the regular responses
+// Arguments are Modbus server ID, the function code requested, the message data and length of it,
+// plus a user-supplied token to identify the causing request
+void handleData(ModbusMessage response, uint32_t token)
+{
+ // Serial.printf("Response: serverID=%d, FC=%d, Token=%08X, length=%d:\n", response.getServerID(), response.getFunctionCode(), token, response.size());
+ for (auto &byte : response)
+ {
+ // Serial.printf("%02X ", byte);
+ }
+ // debugln("");
+
+ switch (response.getServerID())
+ {
+ case 1:
+ // Statement(s)
+
+ // debugln("*****");
+ response.get(3, DS3_Pow_L1);
+ // debug("Watt L1 : ");
+ // debugln(DS3_Pow_L1 / 10);
+ // debugln("*****");
+ Voltaik_Surplus_L1 = (DS3_Pow_L1 / 10) - Basic_load_L1;
+ // debug("Surplus gerechnet L1 : ");
+ // debugln(Voltaik_Surplus_L1);
+
+ break;
+
+ case 2:
+ // debugln("*****");
+ response.get(3, DS3_Pow_L2);
+ // debug("Watt L2 : ");
+ // debugln(DS3_Pow_L2 / 10);
+ // debugln("*****");
+ Voltaik_Surplus_L2 = (DS3_Pow_L2 / 10) - Basic_load_L2;
+ // debug("Surplus gerechnet L2 : ");
+ // debugln(Voltaik_Surplus_L2);
+ break;
+
+ case 3:
+ // debugln("*****");
+ response.get(3, DS3_Pow_L3);
+ // debug("Watt L3 : ");
+ // debugln(DS3_Pow_L3 / 10);
+ // debugln("*****");
+ Voltaik_Surplus_L3 = (DS3_Pow_L3 / 10) - Basic_load_L3;
+ // debug("Surplus gerechnet L3 : ");
+ // debugln(Voltaik_Surplus_L3);
+ }
+
+ DS3_Pow_tot = (DS3_Pow_L1 + DS3_Pow_L2 + DS3_Pow_L3) / 10;
+
+ // debugln();
+ // debug("Watt_tot : ");
+ // debugln(DS3_Pow_tot);
+ // debugln();
+
+ DS3_Pow_L1_L2 = (DS3_Pow_L1 + DS3_Pow_L2) / 10;
+ // debugln();
+ // debug("Watt_L1_L2 : ");
+ // debugln(DS3_Pow_L1_L2);
+ // debugln();
+
+ DS3_Pow_L2_L3 = (DS3_Pow_L2 + DS3_Pow_L3) / 10;
+ // debugln();
+ // debug("Watt_L2_L3 : ");
+ // debugln(DS3_Pow_L2_L3);
+ // debugln();
+
+ DS3_Pow_L1_L3 = (DS3_Pow_L1 + DS3_Pow_L3) / 10;
+ // // debugln();
+ // debug("Watt_L1_L3 : ");
+ // debugln(DS3_Pow_L1_L3);
+ // debugln();
+}
+
+// Define an onError handler function to receive error responses
+// Arguments are the error code returned and a user-supplied token to identify the causing request
+void handleError(Error error, uint32_t token)
+{
+ // ModbusError wraps the error code and provides a readable error message for it
+ ModbusError me(error);
+ Serial.printf("Error response: %02X - %s token: %d\n", (int)me, (const char *)me, token);
+}
+
+// Setup() - initialization happens here
+
+void subscriptions()
+{
+ MQTT_Client.subscribe(Subscription_1);
+ MQTT_Client.subscribe(Subscription_2);
+ // MQTT_Client.subscribe(Subscription_3);
+ // MQTT_Client.subscribe(Subscription_4);
+ // MQTT_Client.subscribe(Subscription_5);
+}
+
+void mqtt_reconnect()
+{ // Loop until reconnected
+ while (!MQTT_Client.connected())
+ {
+ debug("Attempting MQTT connection...");
+ if (MQTT_Client.connect(MQTT_CLIENT_ID))
+ { // Attempt to connect
+ debugln("connected");
+ MQTT_Client.publish(MQTT_OUT_TOPIC, "connected");
+ // MQTT_Client.subscribe(MQTT_IN_TOPIC); // ... and resubscribe
+
+ subscriptions();
+ }
+ else
+ {
+ debugln("failed, rc=" + String(MQTT_Client.state()) +
+ " try again in 5 seconds");
+ delay(5000); // Wait 5 seconds before retrying
+ }
+ }
+}
+
+void mqtt_callback(char *topic, byte *payload, unsigned int length)
+{
+
+ debug("Message arrived in topic: ");
+ debugln(topic);
+
+ debug("Subscribe JSON payload:");
+ for (int i = 0; i < length; i++)
+ {
+ debug((char)payload[i]);
+ }
+
+ debugln();
+ debugln("-----------------------");
+
+ deserializeJson(doc, (byte *)payload, length); // parse MQTT message
+ // deserializeJson(doc,str); //can use string instead of payload
+
+ var_Bezug_tot = doc["act_pwr_imported_p_plus_value"];
+
+ // debug("Bezug tot var : ");
+ // debugln(var_Bezug_tot)*10000;
+ if ((var_Bezug_tot) != 0)
+ {
+ val_Bezug_tot = var_Bezug_tot;
+ }
+ msg = 1; // message flag = 1 when new subscribe message received
+
+ var_Einsp_tot = doc["act_pwr_exported_p_minus_value"];
+ // debug("Einspeis tot var : ");
+ // debugln(var_Einsp_tot)*10000;
+
+ if ((var_Einsp_tot) != 0)
+ {
+ val_Einsp_tot = var_Einsp_tot;
+ }
+ msg = 2;
+ /*
+ var_Einsp_L1 = doc["smarty-JMV/act_pwr_exp_p_minus_l1"];
+ //if ((var_Einsp_L1) != 0)
+ //{
+
+ val_Einsp_L1 = var_Einsp_L1;
+ //}
+ msg_L1 = 1;
+ var_Einsp_L2 = doc["smarty-JMV/act_pwr_exp_p_minus_l2"];
+ if ((var_Einsp_L2) != 0)
+ {
+ val_Einsp_L2 = var_Einsp_L2;
+ }
+ msg_L2 = 1;
+ var_Einsp_L3 = doc["smarty-JMV/act_pwr_exp_p_minus_l3"];
+ if ((var_Einsp_L3) != 0)
+ {
+ val_Einsp_L3 = var_Einsp_L3;
+ }
+ msg_L3 = 1;
+ */
+}
+
+void printDirectory(File dir, int numTabs = 3);
+
+void printDirectory(File dir, int numTabs)
+{
+ while (true)
+ {
+
+ File entry = dir.openNextFile();
+ if (!entry)
+ {
+ // no more files
+ break;
+ }
+ for (uint8_t i = 0; i < numTabs; i++)
+ {
+ debug('\t');
+ }
+ debug(entry.name());
+ if (entry.isDirectory())
+ {
+ debugln("/");
+ printDirectory(entry, numTabs + 1);
+ }
+ else
+ {
+ // files have sizes, directories do not
+ Serial.print("\t\t");
+ Serial.println(entry.size(), DEC);
+ }
+ entry.close();
+ }
+}
+
+void setup()
+{
+ // Init Serial monitor
+ Serial.begin(115200);
+ while (!Serial)
+ {
+ }
+ debugln("__ OK __");
+
+ // Connect to WiFi
+ WiFi.begin(ssid, pass);
+ delay(200);
+ while (WiFi.status() != WL_CONNECTED)
+ {
+ debug(". ");
+ delay(1000);
+ }
+
+ // Initialize LITTLEFS
+
+ /* if (!LITTLEFS.begin(true))
+ {
+ debugln("An Error has occurred while mounting SPIFFS");
+ return;
+ }
+
+ */
+
+ Serial.begin(115200);
+
+ delay(500);
+
+ debugln(F("Inizializing FS..."));
+ if (LITTLEFS.begin())
+ {
+ debugln(F("done."));
+ }
+ else
+ {
+ debugln(F("fail."));
+ }
+ debugln("File sistem info.");
+
+ debug("Total space: ");
+ debug(totalBytes);
+ debugln("byte");
+
+ debug("Total space used: ");
+ debug(usedBytes);
+ debugln("byte");
+
+ Serial.print("Total space free: ");
+
+ debugln("byte");
+
+ debugln();
+
+ // Open dir folder
+ File dir = LITTLEFS.open("/");
+
+ // Cycle all the content
+ printDirectory(dir);
+
+ // init_wifi();
+ MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT);
+ MQTT_Client.setCallback(mqtt_callback);
+
+ ////////////
+ // Send web page with input fields to client
+
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(LITTLEFS, "/index.html", "text/html", false); });
+
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(LITTLEFS, "/index.html", "text/html", processor); });
+
+ // Send a GET request to /get?inputString=
+ server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request)
+ {
+ String inputMessage;
+ // GET inputString value on /get?inputString=
+ if (request->hasParam(PARAM_STRING)) {
+ inputMessage = request->getParam(PARAM_STRING)->value();
+ writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt=
+ else if (request->hasParam(PARAM_INT)) {
+ inputMessage = request->getParam(PARAM_INT)->value();
+ writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_FLOAT)) {
+ inputMessage = request->getParam(PARAM_FLOAT)->value();
+ writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_1=
+ else if (request->hasParam(PARAM_INT_1)) {
+ inputMessage = request->getParam(PARAM_INT_1)->value();
+ writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_2=
+ else if (request->hasParam(PARAM_INT_2)) {
+ inputMessage = request->getParam(PARAM_INT_2)->value();
+ writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_3=
+ else if (request->hasParam(PARAM_INT_3)) {
+ inputMessage = request->getParam(PARAM_INT_3)->value();
+ writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_4=
+ else if (request->hasParam(PARAM_INT_4)) {
+ inputMessage = request->getParam(PARAM_INT_4)->value();
+ writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Setpoint_1)) {
+ inputMessage = request->getParam(PARAM_Setpoint_1)->value();
+ writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kp)) {
+ inputMessage = request->getParam(PARAM_Kp)->value();
+ writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Ki)) {
+ inputMessage = request->getParam(PARAM_Ki)->value();
+ writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kd)) {
+ inputMessage = request->getParam(PARAM_Kd)->value();
+ writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str());
+ }
+
+ else {
+ inputMessage = "No message sent";
+ }
+ debugln(inputMessage);
+ request->send(200, "text/text", inputMessage); });
+ server.onNotFound(notFound);
+ server.begin();
+
+ ///////////////
+
+ IPAddress wIP = WiFi.localIP();
+ Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]);
+
+ // Set up ModbusTCP client.
+ // - provide onData handler function
+ MB.onDataHandler(&handleData);
+ // - provide onError handler function
+ MB.onErrorHandler(&handleError);
+ // Set message timeout to 2000ms and interval between requests to the same host to 200ms
+ MB.setTimeout(2000);
+ // Start ModbusTCP background task
+ MB.setIdleTimeout(10000);
+ MB.setMaxInflightRequests(maxInflightRequests);
+}
+
+// loop() - nothing done here today!
+void loop()
+{
+ static unsigned long lastMillis = 0;
+ if (millis() - lastMillis > 5000)
+ {
+ lastMillis = millis();
+
+ // Create request for
+ // (Fill in your data here!)
+ // - serverID = 1
+ // - function code = 0x03 (read holding register)
+ // - start address to read = word 40084
+ // - number of words to read = 1
+ // - token to match the response with the request. We take the current millis() value for it.
+ //
+ // If something is missing or wrong with the call parameters, we will immediately get an error code
+ // and the request will not be issued
+ Serial.printf("sending request with token %d\n", (uint32_t)lastMillis);
+ Error error;
+ error = MB.addRequest((uint32_t)lastMillis, 1, READ_HOLD_REGISTER, 40084, 1);
+ error = MB.addRequest((uint32_t)lastMillis + 1, 2, READ_HOLD_REGISTER, 40084, 1);
+ error = MB.addRequest((uint32_t)lastMillis + 2, 3, READ_HOLD_REGISTER, 40084, 1);
+ if (error != SUCCESS)
+ {
+ ModbusError e(error);
+ Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e);
+ }
+
+ // Else the request is processed in the background task and the onData/onError handler functions will get the result.
+ //
+ // The output on the Serial Monitor will be (depending on your WiFi and Modbus the data will be different):
+ // __ OK __
+ // . WIFi IP address: 192.168.178.74
+ // Response: serverID=20, FC=3, Token=0000056C, length=11:
+ // 14 03 04 01 F6 FF FF FF 00 C0 A8
+ }
+ if (!MQTT_Client.connected())
+ {
+ mqtt_reconnect();
+ }
+
+ MQTT_Client.loop();
+
+ if (msg == 2)
+ { // check if new callback message
+
+ debug("Msg : ");
+ debugln(msg);
+
+ val_Surplus = (val_Einsp_tot - val_Bezug_tot) * 1000;
+ msg = 0; // reset message flag
+
+ debug("Bezug tot : ");
+ debugln(val_Bezug_tot);
+
+ debug("Einspeis tot : ");
+ debugln(val_Einsp_tot);
+
+ debug("Surplus : ");
+ debugln(val_Surplus);
+
+ if (msg_L1 == 1)
+ {
+ debug("Einspeis L1 : ");
+ debugln(val_Einsp_L1);
+ debug("msg L1 : ");
+ debugln(msg_L1);
+ msg_L1 = 0;
+ }
+ if (msg_L2 == 1)
+ {
+ debug("Einspeis L2 : ");
+ debugln(val_Einsp_L2);
+ msg_L2 = 0;
+ }
+
+ if (msg_L3 == 1)
+ {
+ debug("Einspeis L3 : ");
+ debugln(val_Einsp_L3);
+ msg_L3 = 0;
+ }
+
+ debug("Msg : ");
+ debugln(msg);
+ }
+ // PID or range output.
+
+ if (DAC_1_method == 0)
+ {
+
+ // range 1
+
+ if (val_Surplus >= min_range1)
+ {
+
+ val_DAC_1 = map(val_Surplus, min_range1, max_range1, 0, 255);
+ }
+
+ if (val_Surplus < min_range1)
+ {
+ val_DAC_1 = 0;
+ }
+ }
+ else
+ {
+
+ // PID 1
+
+ float gap = abs(Setpoint_1 - Input_1); // distance away from setpoint
+ if (gap < 10)
+ { // we're close to setpoint, use conservative tuning parameters
+ myPID_1.SetTunings(consKp_1, consKi_1, consKd_1);
+ }
+ else
+ {
+ // we're far from setpoint, use aggressive tuning parameters
+ myPID_1.SetTunings(aggKp_1, aggKi_1, aggKd_1);
+ }
+ myPID_1.Compute();
+ val_DAC_1 = Output_1;
+ }
+
+ if (DAC_2_method == 0)
+ {
+ // range 2
+
+ if (val_inverter >= min_range2)
+ {
+
+ val_DAC_2 = map(val_Surplus, min_range2, max_range2, 0, 255);
+ }
+ if (val_Surplus < min_range2)
+ {
+ val_DAC_2 = 0;
+ }
+ }
+ else
+ {
+
+ // PID 2
+
+ float gap = abs(Setpoint_2 - Input_2); // distance away from setpoint
+ if (gap < 10)
+ { // we're close to setpoint, use conservative tuning parameters
+ myPID_2.SetTunings(consKp_2, consKi_2, consKd_2);
+ }
+ else
+ {
+ // we're far from setpoint, use aggressive tuning parameters
+ myPID_1.SetTunings(aggKp_1, aggKi_1, aggKd_1);
+ }
+ myPID_1.Compute();
+ val_DAC_2 = Output_2;
+ }
+
+ // debug("DAC_1 Wert: ");
+ // debugln(val_DAC_1);
+
+ int yourInputInt1 = readFile(LITTLEFS, "/inputInt_1.txt").toInt();
+ // debug("*** DAC 1 min: ");
+ // debugln(yourInputInt1);
+ min_range1 = yourInputInt1;
+ // debug("min range 1: ");
+ // debugln(min_range1);
+
+ int yourInputInt2 = readFile(LITTLEFS, "/inputInt_2.txt").toInt();
+ // debug("*** DAC 1 max: ");
+ // debugln(yourInputInt2);
+ max_range1 = yourInputInt2;
+ // debug("max range 1: ");
+ // debugln(max_range1);
+
+ int yourInputInt3 = readFile(LITTLEFS, "/inputInt_3.txt").toInt();
+ // debug("*** DAC 2 min: ");
+ // debugln(yourInputInt3);
+ min_range2 = yourInputInt3;
+ // debug("min range 2: ");
+ // debugln(min_range2);
+
+ int yourInputInt4 = readFile(LITTLEFS, "/inputInt_4.txt").toInt();
+ // debug("*** DAC 2 max: ");
+ // debugln(yourInputInt4);
+ max_range2 = yourInputInt4;
+ // debug("max range 2: ");
+ // debugln(max_range2);
+
+ int oldvalue_1;
+ if (select_Input_1 != oldvalue_1)
+ {
+ switch (select_Input_1)
+ {
+
+ case 0:
+ debugln("input_1 case 0 ");
+ break;
+
+ case 1:
+ debugln("input_1 case 1 ");
+ break;
+
+ case 2:
+ debugln("input_1 case 2 ");
+ break;
+
+ case 3:
+ debugln("input_1 case 3 ");
+ break;
+
+ case 4:
+ debugln("input_1 case 4 ");
+ break;
+
+ case 5:
+ debugln("input_1 case 5 ");
+ break;
+
+ case 6:
+ debugln("input_1 case 6 ");
+ break;
+
+ case 7:
+ debugln("input_1 case 7 ");
+ break;
+
+ case 8:
+ debugln("input_1 case 8 ");
+ break;
+
+ case 9:
+ debugln("input_1 case 9 ");
+ break;
+
+ case 10:
+ debugln("input_1 case 10 ");
+ break;
+
+ case 11:
+ debugln("input_1 case 11 ");
+ break;
+ }
+
+ oldvalue_1 = select_Input_1;
+ debug("oldvalue_1 :");
+ debugln(oldvalue_1);
+ delay(2000);
+ }
+} // end loop
\ No newline at end of file
diff --git a/docs/routers/LSA/Regler_pwm_will/src/special_settings.h b/docs/routers/LSA/Regler_pwm_will/src/special_settings.h
new file mode 100644
index 0000000..06e9db2
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/special_settings.h
@@ -0,0 +1,162 @@
+// switch on (1)or of (0) serial.print = serial.print replaced by debug //
+#define DEBUG 0
+#define DEBUGf 0
+#define MQTT_DEBUG 0
+#define SMARTY_DEBUG 0
+#define MODBUS_DEBUG 0
+
+#define PID_DEBUG 0
+#define SSR_DEBUG 0
+#define PWM_DEBUG 0
+#define I2C_DEBUG 0
+
+
+// -- Web server
+// const char *http_username = "admin";
+// const char *http_password = "admin";
+// AsyncWebServer httpServer(80);
+// WebSocketsServer wsServer = WebSocketsServer(81);
+
+// Set your Static IP address
+IPAddress local_IP(192, 168, 178, 93);
+// Set your Gateway IP address
+IPAddress gateway(192, 168, 178, 1);
+
+IPAddress subnet(255, 255, 255, 0);
+IPAddress primaryDNS(192, 168, 178, 1); // optional
+IPAddress secondaryDNS(8, 8, 4, 4); // optional
+
+const char *OTA_host = "Voltaik_PWM_Control"; // Name unter welchem der virtuelle Port in der Arduino-IDE auftaucht
+
+// Modbus Kostal (R)
+IPAddress Modbus_ip = {192, 168, 178, 97}; // IP address of modbus server
+uint16_t Modbus_port = 502; // port of modbus server
+
+// switch on (1)or of (0) Modbus reading not yet implemented
+
+#define Modbus_on_off 0 // switch on (1)or of (0)
+
+const int Modbus_ID_1 = 1; // Kostal Modbus ID
+const int Modbus_ID_2 = 2; // Modbus ID
+const int Modbus_ID_3 = 3; // Modbus ID
+const int Modbus_ID_4 = 4; // Modbus ID
+
+int Adresse_Modbus_Register_1 = 30017;//Kostal Modbus Adresse Watt L1
+int Adresse_Modbus_Register_2 = 30021;//Kostal Modbus Adresse Watt L2
+int Adresse_Modbus_Register_3 = 30025;//Kostal Modbus Adresse Watt L3
+int Adresse_Modbus_Register_4 = 30030;//Kostal Modbus Adresse Watt Total
+
+
+// MQTT
+const char *MQTT_SERVER = "192.168.178.100";
+const char *MQTT_CLIENT_ID = "Smarty_esp_Voltaik";
+const char *MQTT_OUT_TOPIC = "Voltaik_Surplus";
+
+const short MQTT_PORT = 1883; // TLS=8883
+
+const char *MQTT_IN_TOPIC = "#";
+
+// Smarty 1
+
+const char *Subscription_will = "smarty-1/lastwill/onlinestatus";
+const char *Subscription_1 = "smarty-1/power_excess_solar_calc_W";
+const char *Subscription_2 = "smarty-1/power_excess_solar_l1_calc_W";
+const char *Subscription_3 = "smarty-1/power_excess_solar_l2_calc_W";
+const char *Subscription_4 = "smarty-1/power_excess_solar_l3_calc_W";
+const char *Subscription_5 = "smarty-1/act_pwr_exported_p_minus_kW";
+
+
+// Boiler
+//const char *Subscription_6 = "Warmwater-Mer/upper_temp";
+/*
+const char *Subscription_7 = "Warmwater-Mer/lower_temp";
+const char *Subscription_8 = "Warmwater-Mer/max_temp";
+const char *Subscription_9 = "Warmwater-Mer/test_1";
+const char *Subscription_10 = "Warmwater-Mer/test_2";
+const char *Subscription_11 = "Warmwater-Mer/Test_3";
+*/
+
+// SSR_1 SSR_2
+// values have to be set on webpage index.html
+
+// SSR_1 PWM
+
+int PWM_Freq = 1000; /* 1-3 KHz allowed */
+int PWM_Channel_0 = 0;
+int PWM_Resolution = 12;
+
+//
+//
+int SSR_1_mode; // 0=digital 1=PWM mapped 2=Pwm PID
+
+// SSR_1 PID Range 0-4095
+float PID_min = 0;
+float PID_max = 4095;
+float SSR_1_setpoint_distance = 10;
+float SSR_1_PID_direction; // 0 direct (default) or 1 reverse
+
+uint32_t SampleTimeUs = 15000000;
+
+int SSR_2_mode; // 0=digital 1=PWM mapped 2=Pwm PID attention only 0 implemented
+
+int select_Input_SSR_1;
+/*
+ 0 = no input
+ 1 = power_excess_solar_calc_W
+ 2 = power_excess_solar_l1_calc_W
+ 3 = power_excess_solar_l2_calc_W
+ 4 = power_excess_solar_l3_calc_W
+ 5 = production Kostal Surplus L1
+ 6 = production Kostal Surplus L2
+ 7 = production Kostal Surplus L3
+ 8 = production Kostal Surplus L1_L2_L3
+ 9 = production Kostal Surplus L1_L2
+ 10 = production Kostal Surplus L1_L3
+ 11 = production Kostal Surplus L2_L3
+ 12 = test_val_1
+ 13 = test_val_2
+*/
+int select_Input_SSR_2;
+/*
+ 0 = no input
+ 1 = power_excess_solar_calc_W
+ 2 = power_excess_solar_l1_calc_W
+ 3 = power_excess_solar_l2_calc_W
+ 4 = power_excess_solar_l3_calc_W
+ 5 = production Kostal Surplus L1
+ 6 = production Kostal Surplus L2
+ 7 = production Kostal Surplus L3
+ 8 = production Kostal Surplus L1_L2_L3
+ 9 = production Kostal Surplus L1_L2
+ 10 = production Kostal Surplus L1_L3
+ 11 = production Kostal Surplus L2_L3
+ 12 = test_val_1
+13 = test_val_2
+*/
+
+
+int offset_DAC_2 = 2000;// need for getting positive values in reverse
+/*
+Controller Action
+
+If a positive error increases the controller’s output, the controller is said to be direct acting (i.e. heating process).
+When a positive error decreases the controller’s output, the controller is said to be reverse acting (i.e. cooling process).
+Since the PWM and ADC peripherals on microcontrollers only operate with positive values, QuickPID only uses positive values for Input, Output and Setpoint.
+When the controller is set to REVERSE acting, the sign of the error and dInput (derivative of Input) is internally changed. All operating ranges and limits remain the same.
+To simulate a REVERSE acting process from a process that’s DIRECT acting, the Input value needs to be “flipped”.
+That is, if your reading from a 10-bit ADC with 0-1023 range, the input value used is (1023 - reading). See the example AutoTune_RC_Filter.ino for details.
+*/
+uint32_t SampleTimeUs_2 = 15000000;
+
+
+
+//stune settimgs user settings
+uint32_t settleTimeSec = 10;
+uint32_t testTimeSec = 500;
+const uint16_t samples = 500;
+const float inputSpan = 15;
+const float outputSpan = 4095;
+float outputStart = 0;
+float outputStep = 3;
+float tempLimit = 75;
+
diff --git a/docs/routers/LSA/Regler_pwm_will/src/webpage1.h b/docs/routers/LSA/Regler_pwm_will/src/webpage1.h
new file mode 100644
index 0000000..d371bb6
--- /dev/null
+++ b/docs/routers/LSA/Regler_pwm_will/src/webpage1.h
@@ -0,0 +1,650 @@
+// webpage Input
+
+const char *PARAM_INPUT_1 = "input_1";
+const char *PARAM_INPUT_2 = "input_2";
+const char *PARAM_INPUT_3 = "input_3";
+const char *PARAM_INPUT_4 = "input_4";
+
+const char *PARAM_INT_1 = "inputInt_1";
+const char *PARAM_INT_2 = "inputInt_2";
+const char *PARAM_INT_3 = "inputInt_3";
+const char *PARAM_INT_4 = "inputInt_4";
+
+const char *PARAM_Setpoint = "Setpoint";
+const char *PARAM_Setpoint_1 = "Setpoint_1";
+const char *PARAM_Setpoint_2 = "Setpoint_2";
+
+const char *PARAM_Kp = "Kp";
+const char *PARAM_Ki = "Ki";
+const char *PARAM_Kd = "Kd";
+
+const char *PARAM_aggKp = "aggKp";
+const char *PARAM_aggKi = "aggKi";
+const char *PARAM_aggKd = "aggKd";
+
+const char *PARAM_Kp_1 = "Kp_1";
+const char *PARAM_Ki_1 = "Ki_1";
+const char *PARAM_Kd_1 = "Kd_1";
+
+const char *PARAM_aggKp_1 = "aggKp_1";
+const char *PARAM_aggKi_1 = "aggKi_1";
+const char *PARAM_aggKd_1 = "aggKd_1";
+
+const char *PARAM_Kp_2 = "Kp_2";
+const char *PARAM_Ki_2 = "Ki_2";
+const char *PARAM_Kd_2 = "Kd_2";
+
+const char *PARAM_aggKp_2 = "aggKp_2";
+const char *PARAM_aggKi_2 = "aggKi_2";
+const char *PARAM_aggKd_2 = "aggKd_2";
+
+const char *PARAM_SSR_1_on = "SSR_1_on";
+const char *PARAM_SSR_2_on = "SSR_2_on";
+const char *PARAM_SSR_1_off = "SSR_1_off";
+const char *PARAM_SSR_2_off = "SSR_2_off";
+
+const char *PARAM_SSR_1_method = "SSR_1_method";
+const char *PARAM_SSR_2_method = "SSR_2_method";
+
+
+const char *PARAM_SSR_1_mode = "SSR_1_mode";
+const char *PARAM_SSR_2_mode = "SSR_2_mode";
+
+const char *PARAM_min_range_1 = "min_range_1";
+const char *PARAM_min_range_2 = "min_range_2";
+const char *PARAM_max_range_1 = "max_range_1";
+const char *PARAM_max_range_2 = "max_range_2";
+
+const char *PARAM_select_Input_SSR_2 = "select_Input_SSR_2";
+const char *PARAM_select_Input_SSR_1 = "select_Input_SSR_1";
+
+const char *PARAM_PWM_Freq = "PWM_Freq";
+const char *PARAM_PWM_Resolution = "PWM_Resolution";
+
+
+const char *PARAM_SSR_1_setpoint_distance = "SSR_1_setpoint_distance";
+const char *PARAM_SSR_1_PID_direction = "SSR_1_PID_direction";
+
+const char *PARAM_STRING = "inputString";
+const char *PARAM_INT = "inputInt";
+const char *PARAM_FLOAT = "inputFloat";
+// end webpage
+
+// Webserver for parameter data input
+
+AsyncWebServer httpServer(80);
+
+void notFound(AsyncWebServerRequest *request)
+{
+ request->send(404, "text/plain", "Not found");
+}
+
+String readFile(fs::FS &fs, const char *path)
+{
+ // Serial.printf("Reading file: %s\r\n", path);
+ File file = fs.open(path, "r");
+ if (!file || file.isDirectory())
+ {
+ debugln("- empty file or failed to open file");
+ return String();
+ }
+ // debugln("- read from file:");
+ String fileContent;
+ while (file.available())
+ {
+ fileContent += String((char)file.read());
+ }
+ file.close();
+ // debugln(fileContent);
+ return fileContent;
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message
+{
+ Serial.printf("Writing file: %s\r\n", path);
+ File file = fs.open(path, "w");
+ if (!file)
+ {
+ debugln("- failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ debugln("- file written");
+ }
+ else
+ {
+ debugln("- write failed");
+ }
+ file.close();
+}
+
+// Replaces placeholder with stored values
+
+String processor(const String &var)
+{
+ debugln(var);
+ debug("var");
+ debugln(var);
+ if (var == "inputString")
+ {
+ return readFile(LITTLEFS, "/inputString.txt");
+ }
+ else if (var == "inputInt")
+ {
+ return readFile(LITTLEFS, "/inputInt.txt");
+ }
+ else if (var == "inputFloat")
+ {
+ return readFile(LITTLEFS, "/inputFloat.txt");
+ }
+ else if (var == "inputInt_1")
+ {
+ return readFile(LITTLEFS, "/inputInt_1.txt");
+ }
+ else if (var == "inputInt_2")
+ {
+ return readFile(LITTLEFS, "/inputInt_2.txt");
+ }
+ else if (var == "inputInt_3")
+ {
+ return readFile(LITTLEFS, "/inputInt_3.txt");
+ }
+ else if (var == "inputInt_4")
+ {
+ return readFile(LITTLEFS, "/inputInt_4.txt");
+ }
+ /////////////
+
+ else if (var == "Setpoint")
+ {
+ return readFile(LITTLEFS, "/Setpoint.txt");
+ }
+ else if (var == "SSR_1_setpoint_distance")
+ {
+ return readFile(LITTLEFS, "/SSR_1_setpoint_distance.txt");
+ }
+
+ else if (var == "Setpoint_1")
+ {
+ return readFile(LITTLEFS, "/Setpoint_1.txt");
+ }
+ else if (var == "Setpoint_2")
+ {
+ return readFile(LITTLEFS, "/Setpoint_2.txt");
+ }
+ /////////
+ else if (var == "Kp")
+ {
+ return readFile(LITTLEFS, "/Kp.txt");
+ }
+ else if (var == "Ki")
+ {
+ return readFile(LITTLEFS, "/Ki.txt");
+ }
+ else if (var == "Kd")
+ {
+ return readFile(LITTLEFS, "/Kd.txt");
+ }
+ else if (var == "aggKp")
+ {
+ return readFile(LITTLEFS, "agg/Kp.txt");
+ }
+ else if (var == "aggKi")
+ {
+ return readFile(LITTLEFS, "/aggKi.txt");
+ }
+ else if (var == "aggKd")
+ {
+ return readFile(LITTLEFS, "/aggKd.txt");
+ }
+
+ /////////
+ else if (var == "Kp_1")
+ {
+ return readFile(LITTLEFS, "/Kp_1.txt");
+ }
+ else if (var == "Ki_1")
+ {
+ return readFile(LITTLEFS, "/Ki_1.txt");
+ }
+ else if (var == "Kd_1")
+ {
+ return readFile(LITTLEFS, "/Kd_1.txt");
+ }
+ else if (var == "aggKp_1")
+ {
+ return readFile(LITTLEFS, "agg/Kp_1.txt");
+ }
+ else if (var == "aggKi_1")
+ {
+ return readFile(LITTLEFS, "/aggKi_1.txt");
+ }
+ else if (var == "aggKd_1")
+ {
+ return readFile(LITTLEFS, "/aggKd_1.txt");
+ }
+ ////////////
+ else if (var == "Kp_2")
+ {
+ return readFile(LITTLEFS, "/Kp_2.txt");
+ }
+ else if (var == "Ki_2")
+ {
+ return readFile(LITTLEFS, "/Ki_2.txt");
+ }
+ else if (var == "Kd_2")
+ {
+ return readFile(LITTLEFS, "/Kd_2.txt");
+ }
+
+ else if (var == "aggKp_2")
+ {
+ return readFile(LITTLEFS, "/aggKp_2.txt");
+ }
+ else if (var == "aggKi_2")
+ {
+ return readFile(LITTLEFS, "/aggKi_2.txt");
+ }
+ else if (var == "aggKd_2")
+ {
+ return readFile(LITTLEFS, "/aggKd_2.txt");
+ }
+ /////////////////////////////
+ else if (var == "SSR_1_method")
+ {
+ return readFile(LITTLEFS, "/SSR_1_method.txt");
+ }
+ else if (var == "SSR_2_method")
+ {
+ return readFile(LITTLEFS, "/SSR_2_method.txt");
+ }
+
+ /////////////////////////////
+ else if (var == "SSR_1_mode")
+ {
+ return readFile(LITTLEFS, "/SSR_1_mode.txt");
+ }
+ else if (var == "SSR_2_mode")
+ {
+ return readFile(LITTLEFS, "/SSR_2_mode.txt");
+ }
+
+ else if (var == "select_Input_SSR_1")
+ {
+ return readFile(LITTLEFS, "/select_Input_SSR_1.txt");
+ }
+ else if (var == "select_Input_SSR_2")
+ {
+ return readFile(LITTLEFS, "/select_Input_SSR_2.txt");
+ }
+ /////////////////////////////
+
+
+
+ //////////
+
+ else if (var == "min_range_1")
+ {
+ return readFile(LITTLEFS, "/min_range_1.txt");
+ }
+ else if (var == "min_range_2")
+ {
+ return readFile(LITTLEFS, "/min_range_2.txt");
+ }
+ else if (var == "max_range_1")
+ {
+ return readFile(LITTLEFS, "/max_range_1.txt");
+ }
+ else if (var == "max_range_2")
+ {
+ return readFile(LITTLEFS, "/max_range_2.txt");
+ }
+ ////////////////////////////////////////
+ /////////////////////////////
+ else if (var == "SSR_1_on")
+ {
+ return readFile(LITTLEFS, "/SSR_1_on.txt");
+ }
+ else if (var == "SSR_2_on")
+ {
+ return readFile(LITTLEFS, "/SSR_2_on.txt");
+ }
+
+
+ /////////////////////////////
+ else if (var == "SSR_1_off")
+ {
+ return readFile(LITTLEFS, "/SSR_1_off.txt");
+ }
+ else if (var == "SSR_2_off")
+ {
+ return readFile(LITTLEFS, "/SSR_2_off.txt");
+ }
+
+ else if (var == "SSR_1_PID_direction")
+ {
+ return readFile(LITTLEFS, "/SSR_1_PID_direction.txt");
+ }
+ else if (var == "SSR_2_PID_direction")
+ {
+ return readFile(LITTLEFS, "/SSR_2_PID_direction.txt");
+ }
+ ///////////////
+ else if (var == "PWM_Freq.txt")
+ {
+ return readFile(LITTLEFS, "/PWM_Freq.txt");
+ }
+ else if (var == "PWM_Resolution.txt")
+ {
+ return readFile(LITTLEFS, "/PWM_Resolution.txt");
+ }
+
+ return String();
+}
+ // Send web page with input fields to client
+
+ httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ {
+ request->send(LITTLEFS, "/index.html", "text/html", false); });
+
+ httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ {
+ request->send(LITTLEFS, "/index.html", "text/html", processor); });
+
+ // httpServer.serveStatic("/", LITTLEFS, "/").setDefaultFile("index.html");//not working see bug
+
+ // Send a GET request to /get?inputString=
+ httpServer.on("/get", HTTP_GET, [](AsyncWebServerRequest *request)
+ {
+ String inputMessage;
+ // GET inputString value on /get?inputString=
+ if (request->hasParam(PARAM_STRING))
+ {
+ inputMessage = request->getParam(PARAM_STRING)->value();
+ writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt=
+ else if (request->hasParam(PARAM_INT))
+ {
+ inputMessage = request->getParam(PARAM_INT)->value();
+ writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_FLOAT))
+ {
+ inputMessage = request->getParam(PARAM_FLOAT)->value();
+ writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_1=
+ else if (request->hasParam(PARAM_INT_1))
+ {
+ inputMessage = request->getParam(PARAM_INT_1)->value();
+ writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_2=
+ else if (request->hasParam(PARAM_INT_2))
+ {
+ inputMessage = request->getParam(PARAM_INT_2)->value();
+ writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_3=
+ else if (request->hasParam(PARAM_INT_3))
+ {
+ inputMessage = request->getParam(PARAM_INT_3)->value();
+ writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str());
+ }
+ // GET inputInt value on /get?inputInt_4=
+ else if (request->hasParam(PARAM_INT_4))
+ {
+ inputMessage = request->getParam(PARAM_INT_4)->value();
+ writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Setpoint_1))
+ {
+ inputMessage = request->getParam(PARAM_Setpoint_1)->value();
+ writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Setpoint_2))
+ {
+ inputMessage = request->getParam(PARAM_Setpoint_2)->value();
+ writeFile(LITTLEFS, "/Setpoint_2.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kp))
+ {
+ inputMessage = request->getParam(PARAM_Kp)->value();
+ writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Ki))
+ {
+ inputMessage = request->getParam(PARAM_Ki)->value();
+ writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kd))
+ {
+ inputMessage = request->getParam(PARAM_Kd)->value();
+ writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str());
+ }
+ //////////
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kp_1))
+ {
+ inputMessage = request->getParam(PARAM_Kp_1)->value();
+ writeFile(LITTLEFS, "/Kp_1.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Ki_1))
+ {
+ inputMessage = request->getParam(PARAM_Ki_1)->value();
+ writeFile(LITTLEFS, "/Ki_1.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kd_1))
+ {
+ inputMessage = request->getParam(PARAM_Kd_1)->value();
+ writeFile(LITTLEFS, "/Kd_1.txt", inputMessage.c_str());
+ }
+ //////////
+ else if (request->hasParam(PARAM_aggKp_1))
+ {
+ inputMessage = request->getParam(PARAM_aggKp_1)->value();
+ writeFile(LITTLEFS, "/aggKp_1.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_aggKi_1))
+ {
+ inputMessage = request->getParam(PARAM_aggKi_1)->value();
+ writeFile(LITTLEFS, "/aggKi_1.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_aggKd_1))
+ {
+ inputMessage = request->getParam(PARAM_aggKd_1)->value();
+ writeFile(LITTLEFS, "agg/Kd_1.txt", inputMessage.c_str());
+ }
+ //////////
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kp_2))
+ {
+ inputMessage = request->getParam(PARAM_Kp_2)->value();
+ writeFile(LITTLEFS, "/Kp_2.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Ki_2))
+ {
+ inputMessage = request->getParam(PARAM_Ki_2)->value();
+ writeFile(LITTLEFS, "/Ki_2.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_Kd_2))
+ {
+ inputMessage = request->getParam(PARAM_Kd_2)->value();
+ writeFile(LITTLEFS, "/Kd_2.txt", inputMessage.c_str());
+ }
+ //////////
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_aggKp_2))
+ {
+ inputMessage = request->getParam(PARAM_aggKp_2)->value();
+ writeFile(LITTLEFS, "/aggKp_2.txt", inputMessage.c_str());
+ }
+
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_aggKi_2))
+ {
+ inputMessage = request->getParam(PARAM_aggKi_2)->value();
+ writeFile(LITTLEFS, "/aggKi_2.txt", inputMessage.c_str());
+ }
+ // GET inputFloat value on /get?inputFloat=
+ else if (request->hasParam(PARAM_aggKd_2))
+ {
+ inputMessage = request->getParam(PARAM_aggKd_2)->value();
+ writeFile(LITTLEFS, "/aggKd_2.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_select_Input_SSR_1))
+ {
+ inputMessage = request->getParam(PARAM_select_Input_SSR_1)->value();
+ writeFile(LITTLEFS, "/Input_SSR_1.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_select_Input_SSR_2))
+ {
+ inputMessage = request->getParam(PARAM_select_Input_SSR_2)->value();
+ writeFile(LITTLEFS, "/Input_SSR_2.txt", inputMessage.c_str());
+ }
+
+
+
+ else if (request->hasParam(PARAM_SSR_1_setpoint_distance))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_setpoint_distance)->value();
+ writeFile(LITTLEFS, "/SSR_1_setpoint_distance.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_SSR_1_PID_direction))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_PID_direction)->value();
+ writeFile(LITTLEFS, "/SSR_1_PID_direction.txt", inputMessage.c_str());
+ }
+
+
+ else if (request->hasParam(PARAM_SSR_1_on))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_on)->value();
+ writeFile(LITTLEFS, "/SSR_1_on.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_SSR_1_off))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_off)->value();
+ writeFile(LITTLEFS, "/SSR_1_off.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_SSR_2_on))
+ {
+ inputMessage = request->getParam(PARAM_SSR_2_on)->value();
+ writeFile(LITTLEFS, "/SSR_2_on.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_SSR_2_off))
+ {
+ inputMessage = request->getParam(PARAM_SSR_2_off)->value();
+ writeFile(LITTLEFS, "/SSR_2_off.txt", inputMessage.c_str());
+ }
+
+
+ /////////////////
+
+ else if (request->hasParam(PARAM_PWM_Freq))
+ {
+ inputMessage = request->getParam(PARAM_PWM_Freq)->value();
+ writeFile(LITTLEFS, "/PWM_Freq.txt", inputMessage.c_str());
+ }
+
+ else if (request->hasParam(PARAM_PWM_Resolution))
+ {
+ inputMessage = request->getParam(PARAM_PWM_Resolution)->value();
+ writeFile(LITTLEFS, "/PWM_Resolution.txt", inputMessage.c_str());
+ }
+ ////////////
+
+ else if (request->hasParam(PARAM_SSR_1_method))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_method)->value();
+ writeFile(LITTLEFS, "/SSR_1_method.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_SSR_2_method))
+ {
+ inputMessage = request->getParam(PARAM_SSR_2_method)->value();
+ writeFile(LITTLEFS, "/SSR_2_method.txt", inputMessage.c_str());
+ }
+
+ ////////////////////
+ else if (request->hasParam(PARAM_SSR_1_mode))
+ {
+ inputMessage = request->getParam(PARAM_SSR_1_mode)->value();
+ writeFile(LITTLEFS, "/SSR_1_mode.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_SSR_2_mode))
+ {
+ inputMessage = request->getParam(PARAM_SSR_2_mode)->value();
+ writeFile(LITTLEFS, "/SSR_2_mode.txt", inputMessage.c_str());
+ }
+
+
+ ////////////////////
+
+ else if (request->hasParam(PARAM_min_range_1))
+ {
+ inputMessage = request->getParam(PARAM_min_range_1)->value();
+ writeFile(LITTLEFS, "/min_range_1.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_min_range_2))
+ {
+ inputMessage = request->getParam(PARAM_min_range_2)->value();
+ writeFile(LITTLEFS, "/min_range_2.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_max_range_1))
+ {
+ inputMessage = request->getParam(PARAM_max_range_1)->value();
+ writeFile(LITTLEFS, "/max_range_1.txt", inputMessage.c_str());
+ }
+ else if (request->hasParam(PARAM_max_range_2))
+ {
+ inputMessage = request->getParam(PARAM_max_range_2)->value();
+ writeFile(LITTLEFS, "/max_range_2.txt", inputMessage.c_str());
+ }
+
+ else
+ {
+ inputMessage = "No message sent";
+ }
+ debugln(inputMessage);
+ request->send(200, "text/text", inputMessage); });
+ httpServer.onNotFound(notFound);
+ httpServer.begin();
+
+ ///////////////
+
+ IPAddress wIP = WiFi.localIP();
+ Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]);
+
+ // Set up ModbusTCP client.
+ // - provide onData handler function
+ MB.onDataHandler(&handleData);
+ // - provide onError handler function
+ MB.onErrorHandler(&handleError);
+ // Set message timeout to 2000ms and interval between requests to the same host to 200ms
+ MB.setTimeout(2000);
+ // Start ModbusTCP background task
+ MB.setIdleTimeout(10000);
+ MB.setMaxInflightRequests(maxInflightRequests);
+}
\ No newline at end of file
diff --git a/docs/routers/LSA/voltaikPID_materiel.pdf b/docs/routers/LSA/voltaikPID_materiel.pdf
new file mode 100644
index 0000000..0c93712
Binary files /dev/null and b/docs/routers/LSA/voltaikPID_materiel.pdf differ
diff --git "a/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf" "b/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf"
new file mode 100644
index 0000000..fd68db4
Binary files /dev/null and "b/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf" differ
diff --git a/docs/routers/Routeur Tignous/V6_cours.ino b/docs/routers/Routeur Tignous/V6_cours.ino
new file mode 100755
index 0000000..7533b15
--- /dev/null
+++ b/docs/routers/Routeur Tignous/V6_cours.ino
@@ -0,0 +1,361 @@
+// V6 cours VERSION FRANCAISE SIMPLIFIEE UNIQUEMENT EN COMMANDE DE PHASE. du 27 / 06 /19
+
+// + CORRECTION du SM Ă faible PUISSANCE ROUTEE .
+// + DECLENCHEMENT d'une 2eme CHARGE en ZERO CROSSING suivant un seuil haut et un seuil bas
+// Ă partir de l'info "Fd"
+// version fast 1000 joules
+
+ /* Pour dériver du surplus solaire vers un chauffe eau utilisant un relai statique non passage à zero.
+ *
+ PRINCIPE =Calculer une cinquantaine de fois par période la puissance instantannée
+ au noend de raccordement;moyenner sur le cycle puis en déduire le paquet d'énergie à ajouter ou soustraire
+ au "bucket".
+ A l'intérieur d'une fourchette de 100 à 1000 joules ,un triac est activé plus ou moins tard
+ dérivant vers une charge extérieure la quantité exacte d'énergie de façon à retorter
+ ou exporter un minimum.
+ Les échantillons doivent être décales (offset)et filtrés pour être numérisés par l'Atmel
+ */
+
+#include
+#define POSITIVE 1
+#define NEGATIVE 0
+#define ON 1 // commande positive du relai statique ou triac
+#define OFF 0
+byte CdeCh1 = 2;// pin4 micro cde SSR1
+byte CdeCh2 = 4;// 8 pin 14 micro cde SSR2 (ou 4 pin 6)
+byte voltageSensorPin = A3;
+byte currentSensorPin = A5;
+float SommeP = 0; //somme de P1 sur N periodes
+int SMC =-60; // Safety Margin Compensée
+float ret = 0 ; // retard
+
+float imaP = 0; //image de P pour calcul de SMC
+long cycleCount = 0;
+long cptperiodes = 0;
+int samplesDuringThisMainsCycle = 0;
+byte nextStateOfTriac;
+float cyclesPerSecond = 50; // flottant pour plus de précision
+int seuilH; //seuil enclenchemen ch2
+int seuilB; //seuil déclenchemen ch2
+int CE ; // puissance Chauffe eau
+int KCE ; // coefficient puissance kCE = 8000 / CE
+byte polarityNow;
+
+boolean flg2 = false ; // pulse cde 2
+boolean triggerNeedsToBeArmed = false;
+boolean beyondStartUpPhase = false;
+float Plissee = 0;
+float energyInBucket = 0;
+float rPWRprec ; //realPower de la période précédente
+int capacityOfEnergyBucket = 1000; // AU LIEU DE 3600
+int recovPWR = 500 ;// VOIR
+int sampleV,sampleI; // Les valeurs de tension et courant sont des entiers dans l'espace 0 ...1023
+int lastSampleV; // valeur à la boucle précédente.
+float lastFilteredV,filteredV; // tension après filtrage pour retirer la composante continue
+float prevDCoffset; // <<--- pour filtre passe bas
+float DCoffset; // <<--- idem
+float cumVdeltasThisCycle; // <<--- idem
+float sampleVminusDC; // <<--- idem
+float sampleIminusDC; // <<--- idem
+float lastSampleVminusDC; // <<--- idem
+float sumP; // Somme cumulée des puissances à l'intérieur d'un cycle.
+// realpower est la puissance moyenne à l'intérieur d'une période
+
+int PHASECAL; //correction de phase inutilisé = 1
+float POWERCAL; // pour conversion des valeur brutes V et I en joules.
+int VOLTAGECAL; // Pour determiner la tension mini de déclenchement du triac. // the trigger device can be safely armed
+
+boolean firstLoopOfHalfCycle;
+boolean phaseAngleTriggerActivated;
+unsigned long Tc; // = To machine à chaque début de 1/2 periode
+unsigned long Fd; // firing delay
+
+
+void setup()
+{
+
+ wdt_enable(WDTO_8S);
+
+ Serial.begin(500000); // pour tests
+ pinMode(CdeCh1, OUTPUT);
+ pinMode(CdeCh2, OUTPUT);
+
+
+ //++++++++ PARAMETRES A MODIFIER SUIVANT INSTALL++++++
+
+ CE = 2400 ; // Puissance chauffe eau
+ seuilH =500; //seuil enclenchemen ch2 en W approximatifs
+ seuilB =100; //seuil déclenchemen ch2
+
+// ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+ KCE = 8000 / (CE) ; // coefficient suivant la puissance du CE pour la définition des seuils
+
+ POWERCAL = 0.18 ; // org 0.12 Ă ajuster pour faire coincider la puissance vraie avec le realPwer.
+ // en utilisant le traceur serie.
+ // NON CRITIQUE car les valeur absolues s'annulent en phase de régulation // retort and export flows are balanced.
+
+ VOLTAGECAL = (float)679 / 471; // En volts par pas d'ADC.
+ // Utilisé pour déterminer quand la tension secteur est suffisante pour
+ // exciter le triac. noter les valeurs min et max de la mesure de tension
+ // par exemple 678.8 pic pic
+ // La dynamique Ă©tant de 471 signifie une sous estimation de la tension
+ // de 471/679. VOLTAGECAL doit donc être multiplié par l'inverse
+ // de 679/471 soit 1.44
+ PHASECAL = 1; // NON CRITIQUE
+
+ sumP = 0 ;
+}
+
+
+void loop() // Une paire tension / courant est mesurée à chaque boucle (environ 54 par période)
+
+ {
+ wdt_reset();
+ samplesDuringThisMainsCycle++; // incrément du nombre d'échantillons par période secteur pour calcul de puissance.
+
+ // Sauvegarde des échantillons précédents
+ lastSampleV=sampleV; // pour le filtre passe haut
+ lastFilteredV = filteredV; // afin d'identifier le début de chaque période
+ lastSampleVminusDC = sampleVminusDC; // for phasecal calculation
+
+// Acquisition d'une nouvelle paire d'chantillons bruts. temps total :380µS
+
+ sampleI = analogRead(currentSensorPin);
+ sampleV = analogRead(voltageSensorPin);
+
+ // Soustraction de la composante continue déterminée par le filtre passe bas
+ sampleVminusDC = sampleV - DCoffset;
+ sampleIminusDC = sampleI - DCoffset;
+
+ // Un filtre passe haut est utilisé pour déterminer le début de cycle.
+ filteredV = 0.996*(lastFilteredV+sampleV-lastSampleV); // Sinus tension reconstituée
+ // lastFilteredV = zéro en début de cycle
+
+ digitalWrite(CdeCh1, OFF); //
+
+ // Détection de la polarité de l'alternance
+ byte polarityOfLastReading = polarityNow;
+
+ if(filteredV >= 0)
+ polarityNow = POSITIVE;
+ else
+ polarityNow = NEGATIVE;
+
+ if (polarityNow == POSITIVE)
+ {
+ if (polarityOfLastReading != POSITIVE)
+ {
+ // C'est le départ d'une nouvelle sinus positive juste après le passage à zéro
+
+ cycleCount++; // incrément Nb de périodes
+ cptperiodes++; // pour affichage toutes les 50 périodes de Power
+ firstLoopOfHalfCycle = true;
+
+ // mise Ă jour du filtre passe bas pour la soustraction de la composante continue
+ prevDCoffset = DCoffset;
+ DCoffset = prevDCoffset + (0.015 * cumVdeltasThisCycle);
+
+ // Calcul la puissance réelle de toutes les mesures echantillonnées durant
+ // le cycle précedent, et determination du gain (ou la perte) en energie.
+ float realPower = POWERCAL * sumP / (float)samplesDuringThisMainsCycle;
+ float realEnergy = realPower / cyclesPerSecond;//Pmoy X 0.02 en joules sur une période
+
+
+ //**********cas de variation brusque et importante ******
+ // pour eviter un pic d'injection , on repart Ă puissance moindre
+ float deltaP = realPower - rPWRprec ; // puissance période - puissance période précédente
+ rPWRprec = realPower ; // mise Ă jour
+ if ( deltaP > recovPWR) // valeur Ă ajuster
+ energyInBucket = 300 ; // routage sur Pmax /3
+
+
+ if (beyondStartUpPhase == true)// > 2 secondes
+ {
+ // Supposant que les filtres ont eu suffisamment de temps de se stabiliser
+ // ajout de cette energie d'une période à l'énergie du reservoir.
+ energyInBucket += realEnergy;
+
+ // Reduction d'énergie dans le reservoir d'une quantité "safety margin"
+ // Ceci permet au système un décalage pour plus d'injection ou + de soutirage
+ energyInBucket -= SMC / cyclesPerSecond;
+
+ // Limites dans la fourchette 0 ...1000 joules ;ne peut être négatif le 11 / 2 / 18
+ if (energyInBucket > capacityOfEnergyBucket)
+ energyInBucket = capacityOfEnergyBucket;
+ if (energyInBucket < 0)
+ energyInBucket = 0;
+ }
+ else
+ {
+ // Après un reset attendre 100 périodes (2 secondes) le temps que la composante continue soit éliminée par le filtre
+ if(cycleCount > 100) // deux secondes
+ beyondStartUpPhase = true; //croisière
+ }
+ triggerNeedsToBeArmed = true; // déclenchement armé à chaque cycle
+
+ // ********************************************************
+
+ // determination du retard de déclenchement du triac
+
+ // Ne pas allumer si l'Ă©nergie du bucket est trop basse (Fd = Firing Delay)
+ if (energyInBucket <= 100)
+ {Fd = 99999;
+ }
+ else
+
+ if (energyInBucket >= 1000)
+ { Fd = 200;// déclencher immediatement si le niveau d'energie est au dessus du max
+
+ }
+
+ // determination du bon point de déclenchement pour un niveau donné
+ // algorithme simple
+ // Fd est le retard au déclenchement en microsecondes du début de la sinus .
+ // pour Pmin = 10000 corrigé à 8500
+ // pour Pmax = 0 corrigé à 200
+
+ else
+
+ {
+ Fd = 10 * (1020 - energyInBucket);
+
+ ret = (Fd);
+ if (ret >= 8000)
+ ret = 8000; // LIMITE BASSE 8000 soit 50W
+ imaP = 8000 - (ret) ;
+
+ SMC = -60 ; //Base de -60
+
+ if ((Fd > 7300)) // <200w compensation pour moindre soutirage Ă basse puissance
+ SMC = -30 ; // -30 par défaut
+
+
+ if (Fd > 8500) // pas de déclenchement
+ { Fd = 99999;
+
+ }
+ }
+ //******************************************************
+ // imaP des SEUILS imaP est une image de la puissance routée (8000-(Fd))/N environ 1500 W max
+
+ // SommeP tient compte de la puissance du Chauffe eau
+ // Plissee est SommeP moyennée sur N périodes
+
+ (SommeP += (imaP / (KCE))) ; // Puissance routée par période secteur et incrémentée
+
+ if (cptperiodes==250) //Pmoy lissée ;par exemple sur 250 secondes
+
+ { Plissee = SommeP / 250 ; //moyenne sur N Ă©chantillons }
+ if (Plissee >= seuilH) //seuil enclenchemen ch2
+ { flg2 = true ;
+ }
+ if (Plissee <= seuilB)
+ { flg2 = false ;
+
+ } // sinon ,on coupe
+
+ // RĂ©initialisation avant un nouveau moyennage
+
+ cptperiodes = 0;
+ SommeP = 0;
+
+ // POUR tests
+ // Serial.print((Plissee), 0);
+ // Serial.print ("\t\t");
+ // Serial.print((KCE), 0) ;
+ // Serial.print ("\t\t");
+ // Serial.print ("\t\t");
+ // Serial.println((imaP), 0) ;
+
+ }
+ sumP = 0;// somme des puissances instantannées
+ samplesDuringThisMainsCycle = 0;
+ cumVdeltasThisCycle = 0;// somme des tensions instantannées filtrées
+ } // Fin du processus spécifique au premier échantillon +ve d'un nouveau cycle secteur
+
+ // suite du traitement des Ă©chantillons de tension POSITIFS ...
+
+ } // Fin du processus sur la demi alternance positive (tension)
+ else
+ {
+ if (polarityOfLastReading != NEGATIVE)
+ {
+ firstLoopOfHalfCycle = true;
+ }
+ }
+ // Processus pour TOUS les échantillons, positifs et negatifs 54 fois par période
+
+
+ unsigned long To = micros(); // Nb de microsSec depuis le lancement du PG
+
+ if (flg2 == true )
+ { {digitalWrite(CdeCh2, ON) ;}}
+ else
+ { {digitalWrite(CdeCh2, OFF) ;}}
+
+if (firstLoopOfHalfCycle == true)
+
+ { Tc = To; // mise à l'heure en début de 1/2 alternance
+ firstLoopOfHalfCycle = false;
+ phaseAngleTriggerActivated = false;
+ // Autre que P max,annuler le déclenchement a la première boucle
+ // de chaque demi cycle pour être sur qu'il ne reste pas bloqué ON.
+ if(Fd > 200)
+
+ { digitalWrite(CdeCh1, OFF);}
+ }
+ if (phaseAngleTriggerActivated == true)
+ {
+ // Sauf demande de puissance max,désarmer le déclenchement a chaque boucle
+ // après la conduction de la demi période. durée du pulse 20000 / 54 = 370µS
+
+ if (Fd > 200)
+ { digitalWrite(CdeCh1, OFF); }
+ }
+ else
+ {
+ if (To >= (Tc + Fd))
+ { digitalWrite(CdeCh1, ON); // at To + Firing delay
+ phaseAngleTriggerActivated = true; }
+
+ }
+ // Fin de la gestion de l'allumage du Triac
+ //*******************************************************
+
+
+ // Apply phase-shift to the voltage waveform to ensure that the system measures a
+ // resistive load with a power factor of unity.
+ float phaseShiftedVminusDC =
+ lastSampleVminusDC + PHASECAL * (sampleVminusDC - lastSampleVminusDC);
+ float instP = phaseShiftedVminusDC * sampleIminusDC; // PUISSANCE derniers échantillons V x I filtrés
+ sumP +=instP; // accumulation à chaque boucle des puissances instantanées dans une période
+
+ cumVdeltasThisCycle += (sampleV - DCoffset); // pour usage avec filtre passe bas
+
+
+} // end of loop()
+
+/*Fd Puissance sur charge 1500 W
+ 8,5 20
+ 8 50
+ 7,5 100
+ 7 200
+ 6.5 300
+ 6 430
+ 5,5 550
+ 5 750
+ 4,5 926
+ 4 1015
+ 3,5 1100
+ 3 1235
+ 2,5 1340
+ 2 1455
+ 0 1500
+ // simple algorithm (with non-linear power response across the energy range)
+// firingDelayInMicros = 10 * (2300 - energyInBucket);
+
+ // complex algorithm which reflects the non-linear nature of phase-angle control.
+ firingDelayInMicros = (asin((-1 * (energyInBucket - 1800) / 500)) + (PI/2)) * (10000/PI);
+
+*/
diff --git a/docs/routers/Routeur Tignous/V7_cours.ino b/docs/routers/Routeur Tignous/V7_cours.ino
new file mode 100755
index 0000000..fe0b5a8
--- /dev/null
+++ b/docs/routers/Routeur Tignous/V7_cours.ino
@@ -0,0 +1,279 @@
+// V7 VERSION FRANCAISE SIMPLIFIEE UNIQUEMENT EN COMMANDE DE PHASE.
+// Rv modifications mars 2021 pour supprimer le soutirage résiduel qui n'a pas d'interet lorsque l'on est pas en CACSI
+
+// + DECLENCHEMENT d'une 2eme CHARGE en ZERO CROSSING suivant un seuil haut et un seuil bas
+// Ă partir de l'info "Fd"
+// version fast 1000 joules
+
+/* Pour dériver du surplus solaire vers un chauffe eau utilisant un relai statique non passage à zero.
+ *
+PRINCIPE =Calculer une cinquantaine de fois par période la puissance instantannée
+au noend de raccordement;moyenner sur le cycle puis en déduire le paquet d'énergie à ajouter ou soustraire
+au "bucket".
+ A l'intérieur d'une fourchette de 100 à 1000 joules ,un triac est activé plus ou moins tard
+dérivant vers une charge extérieure la quantité exacte d'énergie de façon à retorter
+ou exporter un minimum.
+Les échantillons doivent être décales (offset)et filtrés pour être numérisés par l'Atmel
+*/
+
+#include
+#define POSITIVE 1
+#define NEGATIVE 0
+#define ON 1 // commande positive du relai statique ou triac
+#define OFF 0
+byte CdeCh1 = 2; // pin4 micro cde SSR1
+byte CdeCh2 = 4; // 8 pin 14 micro cde SSR2 (ou 4 pin 6)
+byte voltageSensorPin = A3;
+byte currentSensorPin = A5;
+float SommeP = 0; // somme de P1 sur N periodes
+float ret = 0; // retard
+
+float imaP = 0; // image de P pour calcul de SMC
+long cycleCount = 0;
+long cptperiodes = 0;
+int samplesDuringThisMainsCycle = 0;
+byte nextStateOfTriac;
+float cyclesPerSecond = 50; // flottant pour plus de précision
+int seuilH; // seuil enclenchemen ch2
+int seuilB; // seuil déclenchemen ch2
+int CE; // puissance Chauffe eau
+int KCE; // coefficient puissance kCE = 8000 / CE
+byte polarityNow;
+
+boolean flg2 = false; // pulse cde 2
+boolean triggerNeedsToBeArmed = false;
+boolean beyondStartUpPhase = false;
+float Plissee = 0;
+float energyInBucket = 0;
+float rPWRprec; // realPower de la période précédente
+int capacityOfEnergyBucket = 1000; // AU LIEU DE 3600
+int recovPWR = 500; // VOIR
+int sampleV, sampleI; // Les valeurs de tension et courant sont des entiers dans l'espace 0 ...1023
+int lastSampleV; // valeur à la boucle précédente.
+float lastFilteredV, filteredV; // tension après filtrage pour retirer la composante continue
+float prevDCoffset; // <<--- pour filtre passe bas
+float DCoffset; // <<--- idem
+float cumVdeltasThisCycle; // <<--- idem
+float sampleVminusDC; // <<--- idem
+float sampleIminusDC; // <<--- idem
+float lastSampleVminusDC; // <<--- idem
+float sumP; // Somme cumulée des puissances à l'intérieur d'un cycle. // realpower est la puissance moyenne à l'intérieur d'une période
+int PHASECAL; // correction de phase inutilisé = 1
+float POWERCAL; // pour conversion des valeur brutes V et I en joules.
+int VOLTAGECAL; // Pour determiner la tension mini de déclenchement du triac. // the trigger device can be safely armed
+boolean firstLoopOfHalfCycle;
+boolean phaseAngleTriggerActivated;
+unsigned long Tc; // = To machine à chaque début de 1/2 periode
+unsigned long Fd; // firing delay
+
+void setup() {
+ wdt_enable(WDTO_8S);
+ Serial.begin(500000); // pour tests
+ pinMode(CdeCh1, OUTPUT);
+ pinMode(CdeCh2, OUTPUT);
+
+ //++++++++ PARAMETRES A MODIFIER SUIVANT INSTALL++++++
+ CE = 1800; // Puissance chauffe eau
+ seuilH = 500; // seuil enclenchemen ch2 en W approximatifs
+ seuilB = 100; // seuil déclenchemen ch2
+ // ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+ KCE = 8000 / (CE); // coefficient suivant la puissance du CE pour la définition des seuils
+
+ POWERCAL = 0.18; // org 0.12 Ă ajuster pour faire coincider la puissance vraie avec le realPwer.
+ // en utilisant le traceur serie.
+ // NON CRITIQUE car les valeur absolues s'annulent en phase de régulation // retort and export flows are balanced.
+
+ VOLTAGECAL = (float)679 / 471; // En volts par pas d'ADC.
+ // Utilisé pour déterminer quand la tension secteur est suffisante pour
+ // exciter le triac. noter les valeurs min et max de la mesure de tension
+ // par exemple 678.8 pic pic
+ // La dynamique Ă©tant de 471 signifie une sous estimation de la tension
+ // de 471/679. VOLTAGECAL doit donc être multiplié par l'inverse
+ // de 679/471 soit 1.44
+ PHASECAL = 1; // NON CRITIQUE
+
+ sumP = 0;
+}
+
+void loop() // Une paire tension / courant est mesurée à chaque boucle (environ 54 par période)
+{
+ wdt_reset();
+ samplesDuringThisMainsCycle++; // incrément du nombre d'échantillons par période secteur pour calcul de puissance.
+ // Sauvegarde des échantillons précédents
+ lastSampleV = sampleV; // pour le filtre passe haut
+ lastFilteredV = filteredV; // afin d'identifier le début de chaque période
+ lastSampleVminusDC = sampleVminusDC; // for phasecal calculation
+
+ // Acquisition d'une nouvelle paire d'chantillons bruts. temps total :380µS
+ sampleI = analogRead(currentSensorPin);
+ sampleV = analogRead(voltageSensorPin);
+
+ // Soustraction de la composante continue déterminée par le filtre passe bas
+ sampleVminusDC = sampleV - DCoffset;
+ sampleIminusDC = sampleI - DCoffset;
+
+ // Un filtre passe haut est utilisé pour déterminer le début de cycle.
+ filteredV = 0.996 * (lastFilteredV + sampleV - lastSampleV); // Sinus tension reconstituée
+ // lastFilteredV = zéro en début de cycle
+ digitalWrite(CdeCh1, OFF);
+ // Détection de la polarité de l'alternance
+ byte polarityOfLastReading = polarityNow;
+
+ if (filteredV >= 0)
+ polarityNow = POSITIVE;
+ else
+ polarityNow = NEGATIVE;
+
+ if (polarityNow == POSITIVE) {
+ if (polarityOfLastReading != POSITIVE) {
+ // C'est le départ d'une nouvelle sinus positive juste après le passage à zéro
+ cycleCount++; // incrément Nb de périodes
+ cptperiodes++; // pour affichage toutes les 50 périodes de Power
+ firstLoopOfHalfCycle = true;
+
+ // mise Ă jour du filtre passe bas pour la soustraction de la composante continue
+ prevDCoffset = DCoffset;
+ DCoffset = prevDCoffset + (0.015 * cumVdeltasThisCycle);
+
+ // Calcul la puissance réelle de toutes les mesures echantillonnées durant le cycle précedent, et determination du gain (ou la perte) en energie.
+ float realPower = POWERCAL * sumP / (float)samplesDuringThisMainsCycle;
+ float realEnergy = realPower / cyclesPerSecond; // Pmoy X 0.02 en joules sur une période
+ if (beyondStartUpPhase == true) // > 2 secondes
+ {
+ // Supposant que les filtres ont eu suffisamment de temps de se stabiliser ; ajout de cette energie d'une période à l'énergie du reservoir.
+ energyInBucket += realEnergy;
+
+ // Limites dans la fourchette 0 ...1000 joules ;ne peut être négatif le 11 / 2 / 18
+ if (energyInBucket > capacityOfEnergyBucket)
+ energyInBucket = capacityOfEnergyBucket;
+ if (energyInBucket < 0)
+ energyInBucket = 0;
+ } else {
+ // Après un reset attendre 100 périodes (2 secondes) le temps que la composante continue soit éliminée par le filtre
+ if (cycleCount > 100) // deux secondes
+ beyondStartUpPhase = true; // croisière
+ }
+ triggerNeedsToBeArmed = true; // déclenchement armé à chaque cycle
+
+ // ********************************************************
+ // determination du retard de déclenchement du triac
+
+ if (energyInBucket <= 100) // Ne pas allumer si l'Ă©nergie du bucket est trop basse (Fd = Firing Delay)
+ {
+ Fd = 99999;
+ } else
+
+ if (energyInBucket >= 1000) // déclencher immediatement si le niveau d'energie est au dessus du max
+ {
+ Fd = 200;
+ }
+
+ // determination du bon point de déclenchement pour un niveau donné
+ // algorithme simple
+ // Fd est le retard au déclenchement en microsecondes du début de la sinus .
+ // pour Pmin = 10000 corrigé à 8500
+ // pour Pmax = 0 corrigé à 200
+
+ else {
+ Fd = 10 * (1020 - energyInBucket);
+
+ ret = (Fd);
+ if (ret >= 8000)
+ ret = 8000; // LIMITE BASSE 8000 soit 50W
+ imaP = 8000 - (ret);
+ if (Fd > 8500) // pas de déclenchement
+ {
+ Fd = 99999;
+ }
+ }
+ //******************************************************
+ // imaP des SEUILS imaP est une image de la puissance routée (8000-(Fd))/N environ 1500 W max
+
+ // SommeP tient compte de la puissance du Chauffe eau
+ // Plissee est SommeP moyennée sur N périodes
+
+ (SommeP += (imaP / (KCE))); // Puissance routée par période secteur et incrémentée
+
+ if (cptperiodes == 250) // Pmoy lissée ;par exemple sur 250 secondes
+
+ {
+ Plissee = SommeP / 250; // moyenne sur N Ă©chantillons }
+ if (Plissee >= seuilH) // seuil enclenchemen ch2
+ {
+ flg2 = true;
+ }
+ if (Plissee <= seuilB) // sinon ,on coupe
+ {
+ flg2 = false;
+ }
+
+ // RĂ©initialisation avant un nouveau moyennage
+ cptperiodes = 0;
+ SommeP = 0;
+ }
+ sumP = 0; // somme des puissances instantannées
+ samplesDuringThisMainsCycle = 0;
+ cumVdeltasThisCycle = 0; // somme des tensions instantannées filtrées
+ } // Fin du processus spécifique au premier échantillon +ve d'un nouveau cycle secteur
+
+ // suite du traitement des Ă©chantillons de tension POSITIFS ...
+
+ } // Fin du processus sur la demi alternance positive (tension)
+ else {
+ if (polarityOfLastReading != NEGATIVE) {
+ firstLoopOfHalfCycle = true;
+ }
+ }
+ // Processus pour TOUS les échantillons, positifs et negatifs 54 fois par période
+
+ unsigned long To = micros(); // Nb de microsSec depuis le lancement du PG
+
+ if (flg2 == true) {
+ {
+ digitalWrite(CdeCh2, ON);
+ }
+ } else {
+ { digitalWrite(CdeCh2, OFF); }
+ }
+
+ if (firstLoopOfHalfCycle == true)
+
+ {
+ Tc = To; // mise à l'heure en début de 1/2 alternance
+ firstLoopOfHalfCycle = false;
+ phaseAngleTriggerActivated = false;
+ // Autre que P max,annuler le déclenchement a la première boucle
+ // de chaque demi cycle pour être sur qu'il ne reste pas bloqué ON.
+ if (Fd > 200)
+
+ {
+ digitalWrite(CdeCh1, OFF);
+ }
+ }
+ if (phaseAngleTriggerActivated == true) {
+ // Sauf demande de puissance max,désarmer le déclenchement a chaque boucle
+ // après la conduction de la demi période. durée du pulse 20000 / 54 = 370µS
+
+ if (Fd > 200) {
+ digitalWrite(CdeCh1, OFF);
+ }
+ } else {
+ if (To >= (Tc + Fd)) {
+ digitalWrite(CdeCh1, ON); // at To + Firing delay
+ phaseAngleTriggerActivated = true;
+ }
+ }
+ // Fin de la gestion de l'allumage du Triac
+ //*******************************************************
+
+ // Apply phase-shift to the voltage waveform to ensure that the system measures a
+ // resistive load with a power factor of unity.
+ float phaseShiftedVminusDC =
+ lastSampleVminusDC + PHASECAL * (sampleVminusDC - lastSampleVminusDC);
+ float instP = phaseShiftedVminusDC * sampleIminusDC; // PUISSANCE derniers échantillons V x I filtrés
+ sumP += instP; // accumulation à chaque boucle des puissances instantanées dans une période
+
+ cumVdeltasThisCycle += (sampleV - DCoffset); // pour usage avec filtre passe bas
+
+} // end of loop()
diff --git a/docs/routers/Test_sortie_triac_et_zero_crosing.ino b/docs/routers/Test_sortie_triac_et_zero_crosing.ino
new file mode 100644
index 0000000..f889678
--- /dev/null
+++ b/docs/routers/Test_sortie_triac_et_zero_crosing.ino
@@ -0,0 +1,59 @@
+/*
+
+*/
+
+int pushButton =33;
+ int buttonState = 0;
+// the setup function runs once when you press reset or power the board
+void setup() {
+ // pinmode(27, OUTPUT ) = Sortie triac 1 / pinmode(13, OUTPUT ) = Sortie triac 2
+ Serial.begin(115200);
+ pinMode(pushButton, INPUT);
+ pinMode(27, OUTPUT);
+ pinMode(13, OUTPUT);
+}
+
+void test_triac(){
+ digitalWrite(27, HIGH);
+ Serial.println ("La sortie S1 TRIAC ON");
+ delay(1000); // wait for a second
+ digitalWrite(27, LOW); // turn the LED off by making the voltage LOW
+ Serial.println ("La sortie S1 TRIAC OFF");
+ delay(500); // wait for a second
+ digitalWrite(13, HIGH);
+ Serial.println ("La sortie S2 TRIAC ON");
+ delay(1000); // wait for a second
+ digitalWrite(13, LOW);
+ Serial.println ("La sortie S2 TRIAC OFF");
+ delay(500); // wait for a second
+}
+
+
+void test_zeroC(){
+ unsigned long start_times;
+ Serial.println("test Zero crossing :");
+
+ start_times = millis();
+
+ while (digitalRead(pushButton) ) if(millis()>start_times+100) return;
+ while (!digitalRead(pushButton) ) if(millis()>start_times+100) return;
+ start_times = micros();
+ while (digitalRead(pushButton) );
+ start_times = micros()- start_times;
+
+ buttonState += 1;
+ Serial.print ("Compteur Zero crossing :");
+ Serial.println(buttonState);
+ Serial.print ("impulsion Zero crossing : ");
+ Serial.print(start_times);
+ Serial.println (" us");
+ delay(2000);
+}
+
+
+// the loop function runs over and over again forever
+void loop() {
+ test_triac();
+ test_zeroC();
+ delay(1000);
+}
diff --git a/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf b/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf
new file mode 100644
index 0000000..7c1f669
Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf differ
diff --git a/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf b/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf
new file mode 100644
index 0000000..074136b
Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf differ
diff --git a/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt b/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt
new file mode 100644
index 0000000..f63f7c3
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt
@@ -0,0 +1,10 @@
+Notes re. the original circuit diagram for my 3-phase PCB @ rev 1
+
+The original circuit diagram shows R1 = 10K which is outside the
+specified range for the Atmega 328P processor. In a later version
+of the diagram, this value has been increased to 47K.
+
+In the original circuit diagram, resistors R2 - R4 are shown as 10K.
+When operating the processor at 5V, this setup does not make best use
+of the available range of the ADC. In a later version of the diagram,
+these resistors have been increased to 18K.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt b/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt
new file mode 100644
index 0000000..5d2c261
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt
@@ -0,0 +1,3 @@
+Notes re. the original circuit diagram for my 3-phase PCB @ rev 2
+
+There are no known problems with this diagram.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt b/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt
new file mode 100644
index 0000000..308c85b
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt
@@ -0,0 +1,14 @@
+Note re. JeeLib and the RFM69CW
+
+If you are using the newer RFM69CW radio module, be sure to change the sketch to suit.
+A little way below the initial block of comments, find the 2 lines:
+
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+
+Comment out the first line and un-comment the second, so that it reads:
+
+// #define RF69_COMPAT 0 // for the RFM12B
+#define RF69_COMPAT 1 // for the RF69
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt
new file mode 100644
index 0000000..fc42578
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt
@@ -0,0 +1,23 @@
+Detail for my Mk2_3phase_RFdatalog_1.ino sketch
+
+This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are
+repeatedly sampled for each phase in turn. Every mains cycle, the average powers
+for the three separate phases are combined and used to update an "energy bucket".
+This "energy bucket" represents the overall energy state of the premises.
+
+Any surplus energy is made available to dump-loads. This sketch supports
+three dump-loads which can be on any phase. The loads are activated in order
+of priority. An external switch can be used to select between two pre-set
+priority sequences.
+
+Datalogging is supported. This records the average power and Vrms voltage on
+each phase every few seconds. This data is always available at the Serial
+interface. If the RF facility is enabled, this data is also transmitted by RF.
+RF is enabled by including the literal definition for RF_PRESENT near the top
+of the program. If this line is commented out, RF is disabled.
+
+To minimise the rate at which the loads are cycled on and off, this sketch operates
+with a single-threshold anti-flicker algorithm. The optimal rate of cycling is
+determined by the supply meter so may need to be adjusted by the user. The rate at
+which the loads are cycled on and off can be adjusted using the parameter
+postMidPointCrossingDelayForAF_cycles.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt
new file mode 100644
index 0000000..6659fd0
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt
@@ -0,0 +1,33 @@
+Detail for my Mk2_3phase_RFdatalog_1.ino sketch
+
+This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are
+repeatedly sampled for each phase in turn. Every mains cycle, the average powers
+for the three separate phases are combined and used to update an "energy bucket".
+This "energy bucket" represents the overall energy state of the premises.
+
+Any surplus energy is made available to dump-loads. This sketch supports
+three dump-loads which can be on any phase. The loads are activated in order
+of priority. An external switch can be used to select between two pre-set
+priority sequences.
+
+Datalogging is supported. This records the average power and Vrms voltage on
+each phase every few seconds. This data is always available at the Serial
+interface. If the RF facility is enabled, this data is also transmitted by RF.
+RF is enabled by including the literal definition for RF_PRESENT near the top
+of the program. If this line is commented out, RF is disabled.
+
+To minimise the rate at which the loads are cycled on and off, this sketch operates
+with a single-threshold anti-flicker algorithm. The optimal rate of cycling is
+determined by the supply meter so may need to be adjusted by the user. The rate at
+which the loads are cycled on and off can be adjusted using the parameter
+postMidPointCrossingDelayForAF_cycles.
+
+Changes for version _1a:
+When the content of a datalog message is sent to the Serial port, some loss of
+data samples is likely to occur. In version 1a, this block of Serial statements
+has therefore been commented out.
+
+A mechanism has been added which monitors the minimum number of sample sets per
+mains cycle. At 50Hz operation, the expected value is 32. Whenever a datalog message
+is displayed at the Serial port, this value drops to 29 or 30. The correct value
+can be seen when the offending Serial statements are disabled.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt
new file mode 100644
index 0000000..0beccac
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt
@@ -0,0 +1,30 @@
+Detail for my Mk2_3phase_RFdatalog_1.ino sketch
+
+This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are
+repeatedly sampled for each phase in turn. Every mains cycle, the average powers
+for the three separate phases are combined and used to update an "energy bucket".
+This "energy bucket" represents the overall energy state of the premises.
+
+Any surplus energy is made available to dump-loads. This sketch supports
+three dump-loads which can be on any phase. The loads are activated in order
+of priority. An external switch can be used to select between two pre-set
+priority sequences.
+
+Datalogging is supported. This records the average power and Vrms voltage on
+each phase every few seconds. If the RF facility is enabled, this data is
+transmitted by RF. RF is enabled by including the literal definition for
+RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled.
+
+The same data can be sent to the Serial interface, but this could potentially disturb
+the underlying sampling sequence.
+
+Changes for version _2:
+
+- a twin-threshold algorithm for energy state management has been adopted
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- ISR upgraded to prevent a possible timing anomaly
+- a performance checking feature has been added to detect any loss of data
+- the RF69 RF module is now supported
+- the control signals are now active-high to suit the latest 3-phase PCB.
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt
new file mode 100644
index 0000000..2e2454d
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt
@@ -0,0 +1,40 @@
+Detail for my Mk2_3phase_RFdatalog_3.ino sketch
+
+This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are
+repeatedly sampled for each phase in turn. Every mains cycle, the average powers
+for the three separate phases are combined and used to update an "energy bucket".
+This "energy bucket" represents the overall energy state of the premises.
+
+Any surplus energy is made available to dump-loads. This sketch supports
+three dump-loads which can be on any phase. The loads are activated in order
+of priority. An external switch can be used to select between two pre-set
+priority sequences.
+
+Datalogging is supported. This records the average power and Vrms voltage on
+each phase every few seconds. If the RF facility is enabled, this data is
+transmitted by RF. RF is enabled by including the literal definition for
+RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled.
+
+The same data can be sent to the Serial interface, but this could potentially disturb
+the underlying sampling sequence.
+
+Changes for version _2:
+
+- a twin-threshold algorithm for energy state management has been adopted
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- ISR upgraded to prevent a possible timing anomaly
+- a performance checking feature has been added to detect any loss of data
+- the RF69 RF module is now supported
+- the control signals are now active-high to suit the latest 3-phase PCB.
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _3:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - The reported power at each of the phases has been inverted. These values are now in
+ line with the Open Energy Monitor convention, whereby import is positive and
+ export is negative.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt
new file mode 100644
index 0000000..11da624
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt
@@ -0,0 +1,48 @@
+Detail for my Mk2_3phase_RFdatalog_3a.ino sketch
+
+This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are
+repeatedly sampled for each phase in turn. Every mains cycle, the average powers
+for the three separate phases are combined and used to update an "energy bucket".
+This "energy bucket" represents the overall energy state of the premises.
+
+Any surplus energy is made available to dump-loads. This sketch supports
+three dump-loads which can be on any phase. The loads are activated in order
+of priority. An external switch can be used to select between two pre-set
+priority sequences.
+
+Datalogging is supported. This records the average power and Vrms voltage on
+each phase every few seconds. If the RF facility is enabled, this data is
+transmitted by RF. RF is enabled by including the literal definition for
+RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled.
+
+The same data can be sent to the Serial interface, but this could potentially disturb
+the underlying sampling sequence.
+
+Changes for version _2:
+
+- a twin-threshold algorithm for energy state management has been adopted
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- ISR upgraded to prevent a possible timing anomaly
+- a performance checking feature has been added to detect any loss of data
+- the RF69 RF module is now supported
+- the control signals are now active-high to suit the latest 3-phase PCB.
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _3:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - The reported power at each of the phases has been inverted. These values are now in
+ line with the Open Energy Monitor convention, whereby import is positive and
+ export is negative.
+ *
+ * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt
new file mode 100644
index 0000000..201d3d1
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt
@@ -0,0 +1,44 @@
+Detail for my Mk2_3phase_RFdatalog_4.ino sketch
+
+/* Mk2_3phase_RFdatalog_4.ino
+ *
+ * Issue 1 was released in January 2015.
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order. A suitable
+ * output-stage is required for each load; this can be either triac-based, or a
+ * Solid State Relay.
+ *
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes:
+ * - Improved control of multiple loads has been imported from the
+ * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino
+ * - the ISR has been upgraded to fix a possible timing anomaly
+ * - variables to store ADC samples are now declared as "volatile"
+ * - for RF69 RF module is now supported
+ * - a performance check has been added with the result being sent to the Serial port
+ * - control signals for loads are now active-high to suit the latest 3-phase PCB
+ *
+ * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - The reported power at each of the phases has been inverted. These values are now in
+ * line with the Open Energy Monitor convention, whereby import is positive and
+ * export is negative.
+ *
+ * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * July 2022: updated to Mk2_3phase_RFdatalog_4, with this change:
+ * - the datalogging accumulator for Vsquared has been rescaled to 1/16 of its previous value
+ * to avoid the risk of overflowing during a 20-second datalogging period.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt
new file mode 100644
index 0000000..24c3866
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt
@@ -0,0 +1,15 @@
+Detail for my Mk2_RFdatalog_1.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, datalog messages are routinely transmitted by the
+on-board RF facility. By using the pin-saving hardware option, the
+4-digit display is still available for use.
+
+The payload of the transmitted data has three integer fields:
+- the message number, which increases by one every time;
+- the power at the supply point in Joules (import is -ve, export is +ve)
+- the diverted energy total for today, in kWh
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt
new file mode 100644
index 0000000..59ad171
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt
@@ -0,0 +1,30 @@
+Detail for my Mk2_RFdatalog_2.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt
new file mode 100644
index 0000000..567ee82
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt
@@ -0,0 +1,38 @@
+Detail for my Mk2_RFdatalog_3.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt
new file mode 100644
index 0000000..061ed82
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt
@@ -0,0 +1,59 @@
+Detail for my Mk2_RFdatalog_4.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt
new file mode 100644
index 0000000..d611592
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt
@@ -0,0 +1,75 @@
+Detail for my Mk2_RFdatalog_4a.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is positive
+ and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt
new file mode 100644
index 0000000..3dcdd43
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt
@@ -0,0 +1,80 @@
+Detail for my Mk2_RFdatalog_4b.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is
+ positive and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _4b:
+- The variables to store copies of ADC data for use by the main code are now declared as
+ "volatile" to remove any possibility of incorrect operation due to optimisation by the
+ compiler.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt
new file mode 100644
index 0000000..be621c0
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt
@@ -0,0 +1,90 @@
+Detail for my Mk2_RFdatalog_5.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is
+ positive and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _4b:
+- The variables to store copies of ADC data for use by the main code are now declared as
+ "volatile" to remove any possibility of incorrect operation due to optimisation by the
+ compiler.
+
+Changes for version _5:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ - change "triac" to "load" wherever appropriate
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt
new file mode 100644
index 0000000..fc4e014
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt
@@ -0,0 +1,101 @@
+Detail for my Mk2_RFdatalog_5a.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is
+ positive and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _4b:
+- The variables to store copies of ADC data for use by the main code are now declared as
+ "volatile" to remove any possibility of incorrect operation due to optimisation by the
+ compiler.
+
+Changes for version _5:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ - change "triac" to "load" wherever appropriate
+
+Changes for version _5a:
+ - The RF capability is now switchable so that the code will continue to run when an
+ RF module is not fitted. Dataloging can then still place via the Serial port.
+ If the RF module is not accessed correctly, the time-critical logic in the ISR will
+ continue to run in the normal manner. However, the main code will wait forever for a
+ reply which never appears. This will prevent any progress with the RF, Serial or
+ temerature measurements. Surplus power can still be diverted within the ISR to the
+ local load via IO4 at the "trigger" port.
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt
new file mode 100644
index 0000000..3af8352
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt
@@ -0,0 +1,109 @@
+Detail for my Mk2_RFdatalog_6.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is
+ positive and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _4b:
+- The variables to store copies of ADC data for use by the main code are now declared as
+ "volatile" to remove any possibility of incorrect operation due to optimisation by the
+ compiler.
+
+Changes for version _5:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ - change "triac" to "load" wherever appropriate
+
+Changes for version _5a:
+ - The RF capability is now switchable so that the code will continue to run when an
+ RF module is not fitted. Dataloging can then still place via the Serial port.
+ If the RF module is not accessed correctly, the time-critical logic in the ISR will
+ continue to run in the normal manner. However, the main code will wait forever for a
+ reply which never appears. This will prevent any progress with the RF, Serial or
+ temerature measurements. Surplus power can still be diverted within the ISR to the
+ local load via IO4 at the "trigger" port.
+
+Changes for version 6:
+ * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This
+ * allows the datalogging period to be extended beyond 5 seconds without the counter
+ * running out of range.
+ * - diverted energy, as monitored by CT2, is now reported as an average power as well as
+ * the cumulative energy total for the current day.
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt
new file mode 100644
index 0000000..c6dbfcc
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt
@@ -0,0 +1,87 @@
+Detail for my Mk2_RFdatalog_7.ino sketch
+
+ * This sketch is for diverting suplus PV power to a dump load using a triac
+ * or Solid State Relay. Routine datalogging is also supported using the
+ * on-board RF module (either RFM12B or RF69).
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * November 2020: updated to Mk2_RFdatalog_6, with these changes:
+ * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This
+ * allows the datalogging period to be extended beyond 5 seconds without the counter
+ * running out of range.
+ * - diverted energy, as monitored by CT2, is now reported as an average power as well as
+ * the cumulative energy total for the current day.
+ *
+ * July 2022: updated to Mk2_RFdatalog_7, with this change:
+ * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled
+ * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second
+ * datalogging period.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt
new file mode 100644
index 0000000..07721bf
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt
@@ -0,0 +1,109 @@
+Detail for my Mk2_RFdatalog_multiLoad_1.ino sketch
+
+This sketch is based on Mk2_RFdatalog rev 5a for which the
+immediately following notes are relevant:
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the dump-load.
+
+Datalog messages are routinely transmitted by the on-board RF facility.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The _2 release includes various changes since the _1 version:
+
+- The transmitted data now only has two integer fields (no message ID):
+ . the power at the supply point in Joules (import is +ve, export is -ve)
+ . the diverted energy total for today, in kWh
+ (note the change in energy sense to match the OEM convention)
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF transmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- For compatibility with other versions of the Mk2 code, the variable cycleCount
+ has been removed. This variable would have eventually overflowed which
+ could have caused unpredictable effects with other versions of the Mk2 code.
+
+Changes for version _4:
+
+- Code restructured so that all of the main activities are performed within the
+ Interrupt Service Routine. This includes all of the sampling and power diversion
+ functions. Only the slower activities are dealt with by the main code, in loop().
+
+- A checker mechanism records the minimum number of sample sets per mains cycle.
+ For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1}
+ For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4}
+
+- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port.
+- the output mode is now fixed at compile time (unless the "mode" port switch is available)
+- the ADC is now free-running rather than being controlled by a fixed rate timer
+- a persistence check had been added for the zero-crossing detector
+- the payload + check data are displayed to the screen whenever a datalog message is sent
+ (for more details, please consult the code)
+
+- In version_2, an inversion was introduced so that the reported power at the
+ supply point would be in line with the Open Energy Monitor convention (whereby
+ consumption is positive). Unfortunately, this modification has been lost during
+ the version_3 to version_4 upgrade. This change can be easily reinstated by
+ adding after Line #525: tx_data.powerAtSupplyPoint *= -1;
+
+Changes for version _4a:
+
+- During the major restructuring from version _3 to _4, two minor problems were introduced.
+ These have both been fixed by the upgrade to version _4a:
+
+- In version 4, the phaseCal calculation was ineffective because two assignment statements
+ within the restructured ISR routine were in the wrong order. The order of these statements
+ has now been changed so that the phaseCal refinement will again work as intended.
+
+- The inversion of the reported power at the supply point has been reinstated. The reported
+ power value is now in line with the Open Energy Monitor convention, whereby import is
+ positive and export is negative.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _4b:
+- The variables to store copies of ADC data for use by the main code are now declared as
+ "volatile" to remove any possibility of incorrect operation due to optimisation by the
+ compiler.
+
+Changes for version _5:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ - change "triac" to "load" wherever appropriate
+
+Changes for version _5a:
+ - The RF capability is now switchable so that the code will continue to run when an
+ RF module is not fitted. Dataloging can then still place via the Serial port.
+ If the RF module is not accessed correctly, the time-critical logic in the ISR will
+ continue to run in the normal manner. However, the main code will wait forever for a
+ reply which never appears. This will prevent any progress with the RF, Serial or
+ temerature measurements. Surplus power can still be diverted within the ISR to the
+ local load via IO4 at the "trigger" port.
+
+Changes for Mk2_RFdatalog_multiLoad_1:
+- support for temperature sensing commented out;
+- support for an extra load added;
+- load priority code added but commented out as all IO ports are in use.
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt
new file mode 100644
index 0000000..0de56ce
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt
@@ -0,0 +1,84 @@
+Detail for my sketch, Mk2_RFdatalog_multiLoad_1a.ino
+ *
+ * This sketch is for diverting suplus PV power to one or two dump loads using
+ * triac-based output stages or Solid State Relays. Routine datalogging is
+ * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display
+ * showing the Diverted Energy each day is also supported.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RF module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes:
+ * - temperature sensing commented out(formally supported via D3 at the "mode" port")
+ * - support for a second load added (vcontrolled via D3 at the "mode" port")
+ *
+ *
+ * February 2020: updated to Mk2_RFdatalog_multiLoad_1a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt
new file mode 100644
index 0000000..6a89247
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt
@@ -0,0 +1,20 @@
+Detail for my Mk2_bothDisplays_1.ino sketch
+
+This is the original sketch for use with by PCB-based hardware.
+It supports two current sensors. CT1 is for monitoring the flow of
+energy at the supply point. CT2 is available for monitoring the flow
+of current to the dump-load.
+
+The display can be driven in either of two ways. A #define statement
+near the top of the sketch can be either included or commented out to
+achieve this selection. If no display is in use, it doesn't matter
+which way the display code is configured; the driver logic will just
+rattle away in the background, un-noticed by the rest of the world.
+
+If a complete system is purchased from me, this is the sketch that will
+be pre-loaded into its processor. Only the powerCal value(s) will have
+been changed to match the associated hardware.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt
new file mode 100644
index 0000000..9361b2f
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt
@@ -0,0 +1,26 @@
+Detail for my Mk2_bothDisplays_2.ino sketch
+
+This is an tidier version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. A #define statement
+near the top of the sketch can be either included or commented out to
+achieve this selection. If no display is in use, it doesn't matter
+which way the display code is configured; the driver logic will just
+rattle away in the background, un-noticed by the rest of the world.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt
new file mode 100644
index 0000000..f4eaf02
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt
@@ -0,0 +1,35 @@
+Detail for my Mk2_bothDisplays_3.ino sketch
+
+This is an updated version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. The #define statement
+PIN_SAVING_HARDWARE near the top of the sketch can be either
+included or commented out to achieve this selection. If no display is in
+use, it doesn't matter which way this line is included or not.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
+
+Changes for version _3:
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt
new file mode 100644
index 0000000..a99f184
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt
@@ -0,0 +1,38 @@
+Detail for my Mk2_bothDisplays_3a.ino sketch
+
+This is an updated version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. The #define statement
+PIN_SAVING_HARDWARE near the top of the sketch can be either
+included or commented out to achieve this selection. If no display is in
+use, it doesn't matter which way this line is included or not.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
+
+Changes for version _3:
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+Version _3a is for a few typographical changes only.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt
new file mode 100644
index 0000000..47f49a3
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt
@@ -0,0 +1,55 @@
+Detail for my Mk2_bothDisplays_3b.ino sketch
+
+This is an updated version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. The #define statement
+PIN_SAVING_HARDWARE near the top of the sketch can be either
+included or commented out to achieve this selection. If no display is in
+use, it doesn't matter whether this line is included or not.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
+
+
+Changes for version _3:
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss
+of data. The measured value is sent to the Serial port every 5 seconds.
+
+
+Changes for version _3a:
+- typographical changes only.
+
+
+Changes for version _3b:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt
new file mode 100644
index 0000000..bad70ba
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt
@@ -0,0 +1,59 @@
+Detail for my Mk2_bothDisplays_3c.ino sketch
+
+This is an updated version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. The #define statement
+PIN_SAVING_HARDWARE near the top of the sketch can be either
+included or commented out to achieve this selection. If no display is in
+use, it doesn't matter whether this line is included or not.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
+
+
+Changes for version _3:
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss
+of data. The measured value is sent to the Serial port every 5 seconds.
+
+
+Changes for version _3a:
+- typographical changes only.
+
+
+Changes for version _3b:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+
+Changes for version _3c:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt
new file mode 100644
index 0000000..acf2946
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt
@@ -0,0 +1,73 @@
+Detail for my Mk2_bothDisplays_4.ino sketch
+
+This is an updated version of the original sketch for use with my
+PCB-based hardware. It supports two current sensors. CT1 is
+for monitoring the flow of energy at the supply point. CT2 is
+available for monitoring the flow of current to the dump-load.
+
+The display can be driven in either of two ways. The #define statement
+PIN_SAVING_HARDWARE near the top of the sketch can be either
+included or commented out to achieve this selection. If no display is in
+use, it doesn't matter whether this line is included or not.
+
+The two powerCal variables provides a convenient means of calibrating
+hardware for use with a Mk2 Router. CT1 and CT2 each have separate
+powerCal variables, with the suffices _grid and _diverted respectively.
+
+
+Changes for version _2:
+- for compatibility with other versions of the Mk2 code, the variable cycleCount
+has been removed. This variable would have eventually overflowed which
+could have caused unpredictable effects with other versions of the Mk2 code.
+
+- improved description of the display code initialisation in setup() for the
+pin-saving hardware option.
+
+- removal of some unhelpful comments in the IO pin declaration section.
+
+
+Changes for version _3:
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss
+of data. The measured value is sent to the Serial port every 5 seconds.
+
+
+Changes for version _3a:
+- typographical changes only.
+
+
+Changes for version _3b:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+
+Changes for version _3c:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+Changes for version _4:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ - change "triac" to "load" wherever appropriate
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt
new file mode 100644
index 0000000..30f747f
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt
@@ -0,0 +1,65 @@
+Detail for my Mk2_fasterControl_1.ino sketch
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt
new file mode 100644
index 0000000..dc5e558
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt
@@ -0,0 +1,72 @@
+Detail of my sketch, Mk2_fasterControl_1a.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * January 2020: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_1a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt
new file mode 100644
index 0000000..77a95f6
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt
@@ -0,0 +1,77 @@
+Detail of my sketch, Mk2_fasterControl_2.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_1a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * March 2021: updated to Mk2_fasterControl_2 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt
new file mode 100644
index 0000000..2f7e1e1
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt
@@ -0,0 +1,83 @@
+Detail for my sketch, Mk2_fasterControl_3.ino
+
+/* Mk2_fasterControl_3.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_1a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * March 2021: updated to Mk2_fasterControl_2 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * June 2021: updated to Mk2_fasterControl_3 with these changes:
+ * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs,
+ * the value of the parameter lpf_gain has been reduced from 12 to 8.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt
new file mode 100644
index 0000000..aa7eec2
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt
@@ -0,0 +1,102 @@
+Detail for my sketch, Mk2_fasterControl_RFdatalog_1.ino
+
+/* Mk2_fasterControl_RFdatalog_1.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. With this sketch that supports datalogging
+ * via RF, the display can only be used with the pin-saving hardware: ICs 3&4.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes:
+ * - improved multi-load control logic to prevent the primary load from being disturbed by
+ * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line.
+ *
+ * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes:
+ * - addition of datalogging by RF
+ * - removal of the option for standard display hardware (which is incompatible with RF)
+ *
+ * March 2021: updated to Mk2_fasterControl_2 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes:
+ * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs,
+ * the value of the parameter lpf_gain has been reduced from 12 to 8.
+ *
+ * July 2022: updated to Mk2_fasterControl_withRF_4, with this change:
+ * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled
+ * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second
+ * datalogging period.
+ *
+ * September 2022: updated to Mk2_fasterControl_RFdatalog_1, with this change:
+ * - reinstated the code for constraining the energy bucket's level to within its
+ * working range. This important section of code has unfortunately been missing
+ * in all versions of the fasterControl_withRF line. The new name should make the
+ * purpose of this sketch more obvious.
+ *
+ * Robin Emley
+ *
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt
new file mode 100644
index 0000000..fe449a6
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt
@@ -0,0 +1,75 @@
+Detail for my Mk2_fasterControl_twoLoads_1.ino sketch
+
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 20120: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt
new file mode 100644
index 0000000..448e37c
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt
@@ -0,0 +1,79 @@
+Detail for my Mk2_fasterControl_twoLoads_2.ino sketch
+
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 20120: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes:
+ * - improved multi-load control logic to prevent the primary load from being disturbed by
+ * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line.
+ *
+ * * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt
new file mode 100644
index 0000000..1737a1a
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt
@@ -0,0 +1,84 @@
+Detail of my sketch, Mk2_fasterControl_twoLoads_3.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes:
+ * - improved multi-load control logic to prevent the primary load from being disturbed by
+ * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line.
+ *
+ * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ *
+ * * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt
new file mode 100644
index 0000000..323398a
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt
@@ -0,0 +1,89 @@
+Detail for my sketch, Mk2_fasterControl_twoLoads_4.ino
+
+/* Mk2_fasterControl_twoLoads_4.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes:
+ * - improved multi-load control logic to prevent the primary load from being disturbed by
+ * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line.
+ *
+ * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * June 2021: updated to Mk2_fasterControl_twoLoads_4 with these changes:
+ * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs,
+ * the value of the parameter lpf_gain has been reduced from 12 to 8.
+ *
+ *
+ * * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt
new file mode 100644
index 0000000..ff25fdc
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt
@@ -0,0 +1,90 @@
+Detail for my sketch, Mk2_fasterControl_twoLoads_5.ino
+
+/* Mk2_fasterControl_twoLoads_5.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_1a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * March 2021: updated to Mk2_fasterControl_2 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * June 2021: updated to Mk2_fasterControl_3 with these changes:
+ * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs,
+ * the value of the parameter lpf_gain has been reduced from 12 to 8.
+ *
+ * July 2023: updated to Mk2_fasterControl_twoLoads_5 with these changes:
+ * - the ability to control two loads has been transferred from the latest version of my
+ * standard multiLoad sketch, Mk2_multiLoad_wired_7a. The faster control algorithm
+ * has been retained.
+ * The previous 2-load "faster control" sketch (version 4) has been archived as its
+ * behaviour was found to be problematic.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt
new file mode 100644
index 0000000..57ade4f
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt
@@ -0,0 +1,103 @@
+Detail for my sketch Mk2_fasterControl_withRemoteLoad_1.ino
+
+/* Mk2_fasterControl_withRemoteLoad_1.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting suplus PV power to a dump load using a triac or
+ * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on
+ * the OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. With this sketch that supports a remote load
+ * that is controlled via RF, the display can only be used with the pin-saving hardware: ICs 3&4.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_bothDisplays_3c:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_bothDisplays_4, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * November 2019: updated to Mk2_fasterControl_1 with these changes:
+ * - Half way through each mains cycle, a prediction is made of the likely energy level at the
+ * end of the cycle. That predicted value allows the triac to be switched at the +ve going
+ * zero-crossing point rather than waiting for a further 10 ms. These changes allow for
+ * faster switching of the load.
+ * - The range of the energy bucket has been reduced to one tenth of its former value. This
+ * allows the unit's operation to commence more rapidly whenever surplus power is available.
+ * - controlMode is no longer selectable, the unit's operation being effectively hard-coded
+ * as "Normal" rather than Anti-flicker.
+ * - Port D3 now supports an indicator which shows when the level in the energy bucket
+ * reaches either end of its range. While the unit is actively diverting surplus power,
+ * it is vital that the level in the reduced capacity energy bucket remains within its
+ * permitted range, hence the addition of this indicator.
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes:
+ * - the energy overflow indicator has been disabled to free up port D3
+ * - port D3 now supports a second load
+ *
+ * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes:
+ * - improved multi-load control logic to prevent the primary load from being disturbed by
+ * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line.
+ *
+ * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes:
+ * - addition of datalogging by RF
+ * - removal of the option for standard display hardware (which is incompatible with RF)
+ *
+ * March 2021: updated to Mk2_fasterControl_2 with these changes:
+ * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in
+ * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary
+ * to include a 30% boost factor after each change of load state.
+ *
+ * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes:
+ * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs,
+ * the value of the parameter lpf_gain has been reduced from 12 to 8.
+ *
+ * July 2022: updated to Mk2_fasterControl_withRF_4, with this change:
+ * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled
+ * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second
+ * datalogging period.
+ *
+ * September 2022: updated to Mk2_fasterControl_withRemoteLoad_1 with these changes:
+ * - remove all code for datalogging
+ * - add code to support a remote load via RF control. A one-integer on/off instruction can be sent every
+ * mains cycle with a refresh message being sent every 5 mains cycles if the required state of
+ * the load has not changed. For use with the receiver sketch, remoteUnit_fasterControl_n.
+ * - increase the hardware timer duration from 125 us to 150 us (just to reduce the workload)
+ *
+ * Robin Emley
+ *
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt
new file mode 100644
index 0000000..d796896
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt
@@ -0,0 +1,27 @@
+Detail for my Mk2_multiLoad_CAT5_1.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be
+conveniently accessed at the left-hand side of the J1-5 connector on the PCB.
+The uppermost pin is for Load 1, the lowermost one is for Load 6.
+
+By mistake, I posted this code with modified logic to provide active-high
+control signals. This arrangement would be more appropriate for use with
+SSRs rather than with the Motorola MOC3041 which is normally use to drive
+a BTA41 triac. Rather than removing it entirely, this active-high version
+has been relegated to the Archive section. The equivalent active-low version,
+which will is of more relevance to the hardware that is available from this
+website, has been posted as "_2".
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt
new file mode 100644
index 0000000..f10d86c
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt
@@ -0,0 +1,22 @@
+Detail for my Mk2_multiLoad_CAT5_2.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be
+conveniently accessed at the left-hand side of the J1-5 connector on the PCB.
+The uppermost pin is for Load 1, the lowermost one is for Load 6.
+
+For compatibility with previous versions, all loads are driven using
+active-low logic.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt
new file mode 100644
index 0000000..e7e7577
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt
@@ -0,0 +1,28 @@
+Detail for my Mk2_multiLoad_CAT5_3.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be
+conveniently accessed at the left-hand side of the J1-5 connector on the PCB.
+The uppermost pin is for Load 1, the lowermost one is for Load 6.
+
+For compatibility with previous versions, all loads are driven using
+active-low logic.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt
new file mode 100644
index 0000000..5ec6923
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt
@@ -0,0 +1,42 @@
+Detail for my Mk2_multiLoad_CAT5_4.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When using the
+green PCB, these points can all be accessed alongside a 0V pin (with 0.2" spacing).
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt
new file mode 100644
index 0000000..8720404
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt
@@ -0,0 +1,55 @@
+Detail for my Mk2_multiLoad_wired_5.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCB, each of these points can also be accessed alongside a 0V pin
+(with 0.2" spacing). The main(green) board page at www.mk2pvrouter.co.uk has
+details.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5:
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt
new file mode 100644
index 0000000..209e7f2
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt
@@ -0,0 +1,61 @@
+Detail for my Mk2_multiLoad_wired_5a.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCB, each of these points can also be accessed alongside a 0V pin
+(with 0.2" spacing). The main(green) board page at www.mk2pvrouter.co.uk has
+details.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5:
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _5a:
+
+- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are
+ adjusted in the period immediatetly after a change of load state has occurred.
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt
new file mode 100644
index 0000000..9306ca7
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt
@@ -0,0 +1,68 @@
+Detail for my Mk2_multiLoad_wired_6.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCBs, each of these points can be accessed alongside a 0V pin.
+The main PCB page at www.mk2pvrouter.co.uk has details for the access points
+for each of the IO ports.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5: (not to be used)
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _5a: (not to be used)
+
+- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are
+ adjusted in the period immediatetly after a change of load state has occurred.
+
+Changes for version _6:
+
+- Minimum and maximum limits for the energy bucket's level have been reinstated. This
+ section of code had become lost during the upgrade from version 4 to version 5.
+ Without this code in place, neither of the version 5 relesases will operate as
+ intended, so should not be used. Version 6 is believed to be a correct implementation
+ of the improved mechanism for controlling multiple loads.
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt
new file mode 100644
index 0000000..799dae8
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt
@@ -0,0 +1,75 @@
+Detail for my Mk2_multiLoad_wired_6a.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCBs, each of these points can be accessed alongside a 0V pin.
+The main PCB page at www.mk2pvrouter.co.uk has details for the access points
+for each of the IO ports.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5: (not to be used)
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _5a: (not to be used)
+
+- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are
+ adjusted in the period immediatetly after a change of load state has occurred.
+
+Changes for version _6:
+
+- Minimum and maximum limits for the energy bucket's level have been reinstated. This
+ section of code had become lost during the upgrade from version 4 to version 5.
+ Without this code in place, neither of the version 5 relesases will operate as
+ intended, so should not be used. Version 6 is believed to be a correct implementation
+ of the improved mechanism for controlling multiple loads.
+
+Changes for version _6a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt
new file mode 100644
index 0000000..f96c5ac
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt
@@ -0,0 +1,81 @@
+Detail for my Mk2_multiLoad_wired_6b.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCBs, each of these points can be accessed alongside a 0V pin.
+The main PCB page at www.mk2pvrouter.co.uk has details for the access points
+for each of the IO ports.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5: (not to be used)
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _5a: (not to be used)
+
+- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are
+ adjusted in the period immediatetly after a change of load state has occurred.
+
+Changes for version _6:
+
+- Minimum and maximum limits for the energy bucket's level have been reinstated. This
+ section of code had become lost during the upgrade from version 4 to version 5.
+ Without this code in place, neither of the version 5 relesases will operate as
+ intended, so should not be used. Version 6 is believed to be a correct implementation
+ of the improved mechanism for controlling multiple loads.
+
+Changes for version _6a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _6b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt
new file mode 100644
index 0000000..1e38c41
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt
@@ -0,0 +1,90 @@
+Detail for my Mk2_multiLoad_wired_7.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, multiple dump-loads are supported, each being
+controlled by its own dedicated IO pin. To generate sufficient spare
+IO pins for this purpose, the RF facility has been removed from this code.
+By using the pin-saving hardware option, the 4-digit display is still
+available for use.
+
+The primary load (Load 0) is still controlled from the "trigger" port. The
+five IO drivers that have been freed up by not using the RF module have been
+re-allocated to control five additional loads. These signals can be accessed
+at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is
+for Load 1; the lowermost one is for Load 6.
+
+For versions 1 to 3, all loads are driven using active-low logic.
+For version 4, the additional loads (Nos 1 - 5) use active-high logic. When
+using my green PCBs, each of these points can be accessed alongside a 0V pin.
+The main PCB page at www.mk2pvrouter.co.uk has details for the access points
+for each of the IO ports.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+- The 5 additional loads are now driven active-high rather than active-low.
+
+Changes for version _5: (not to be used)
+
+- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n
+- the original twin-threshold algorithm for energy state management has been reinstated;
+- improved mechanism for controlling multiple loads (faster and more accurate);
+- the phaseCal mechanism has been reinstated;
+- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+
+Changes for version _5a: (not to be used)
+
+- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are
+ adjusted in the period immediatetly after a change of load state has occurred.
+
+Changes for version _6:
+
+- Minimum and maximum limits for the energy bucket's level have been reinstated. This
+ section of code had become lost during the upgrade from version 4 to version 5.
+ Without this code in place, neither of the version 5 relesases will operate as
+ intended, so should not be used. Version 6 is believed to be a correct implementation
+ of the improved mechanism for controlling multiple loads.
+
+Changes for version _6a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _6b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+Changes for version _7:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt
new file mode 100644
index 0000000..7425898
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt
@@ -0,0 +1,81 @@
+Detail for my sketch, Mk2_multiLoad_wired_7a.ino
+ *
+ * This sketch is for diverting suplus PV power using multiple hard-wired loads.
+ * An external switch allows either load 0 or Load 1 to have the highest
+ * priority. Any number of loads can be supported by the logic, a dedicated
+ * IO pin being required for each one.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The selector switch, as mentioned above, connects to the "mode" port which has been
+ * re-assigned for priority selection. "Normal" mode can be achieved by setting the
+ * anti-flicker offset prameter to zero at compile-time.
+ *
+ * The integral voltage sensor is fed from one of the secondary coils of the transformer.
+ * Current is measured via Current Transformers at the CT1 and CT1 ports.
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the 'diverted' current, so that energy which is diverted via the primary
+ * dump-load can be recorded and displayed locally.
+ *
+ * A persistence-based 4-digit display is supported. To free up the necessary IO pins
+ * for driving multiple loads, the pin-saving hardware needs to be in place.
+ * These extra logic chips (ICs 3 and 4) reduce the number of IO pins
+ * that are needed to drive the display. The freed-up pins are available at the
+ * J1-5 connector. The uppermost position has been assigned to drive Load 1,
+ * the next one down is for Load 2, and the lowest one is for Load 5. The control signal
+ * for Load 0 is available at the "trigger" connector.
+ *
+ * With the green (rev 2.1) version of my PCB, each of the additional outputs has an
+ * associated ground pin. It is therefore more sensible for those outputs to be active-high
+ * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these
+ * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes:
+ * - reimplementation of cycleCount, as it could have overflowed with unpredictable results;
+ * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied;
+ * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long.
+ *
+ * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed);
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances;
+ * - the logic for each of the 5 additional loads has been inverted by use of the '!' character.
+ * these outputs are now active-high rather than active-low
+ *
+ * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes:
+ * - the original twin-threshold algorithm for energy state management has been reinstated;
+ * - improved mechanism for controlling multiple loads (faster and more accurate);
+ * - the phaseCal mechanism has been reinstated;
+ * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES.
+ *
+ * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change:
+ * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately
+ * after a change of load-state has tqaken place.
+ *
+ * January 2015: renamed as Mk2_multiLoad_wired_6, with this change:
+ * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the
+ * conversion from version 4 to version 5. The absence of this section prevents diversion from starting
+ * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be
+ * a correct implementation of the improved mechanism for controlling multiple loads.
+ *
+ * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to
+ * remove a timing uncertainty.
+ *
+ * January 2016: updated to Mk2_multiLoad_wired_6b:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to Mk2_multiLoad_wired_7, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ *
+ * February 2020: updated to Mk2_multiLoad_wired_7a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt
new file mode 100644
index 0000000..02d6d9d
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt
@@ -0,0 +1,40 @@
+/* Mk2_standardDisplay_3loads_1
+ *
+ * This sketch is for diverting suplus PV power using multiple hard-wired loads. It is intended
+ * for use with my PCB-based hardware for the Mk2 PV Router.
+ *
+ * This sketch is only for use when the hardware is in its standard configuration with 14 wire links
+ * rather than including ICs 3 and 4. The control mode setting is hard-coded prior to compilation,
+ * options being NORMAL and ANTI_FLICKER.
+ *
+ * The integral voltage sensor is fed from one of the secondary coils of the transformer.
+ * Current is measured via Current Transformers at the CT1 and CT2 ports.
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the 'diverted' current, so that energy which is diverted via any of the dump-loads
+ * can be recorded and displayed locally.
+ *
+ * Up to three loads can be supported, the port allocation section has details of where the control signals
+ * can be accessed.
+ *
+ * (earlier history can be found in my multiLoad_wired sketches)
+ *
+ * February 2016: updated to Mk2_multiLoad_wired_7, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ *
+ * February 2020: updated to Mk2_multiLoad_wired_7a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * April 2020: updated to Mk2_standardDisplay_3loads_1 with these changes:
+ * - change the display configuration to standard (i.e. 14 wire links, not pin-saving hardware)
+ * - provide 3 loads at ports D4, D3 and D15 (aka A1)
+ * - control mode is hard-coded to NORMAL, but ANTI_FLICKER mode is still available
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt
new file mode 100644
index 0000000..d717cf8
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt
@@ -0,0 +1,17 @@
+Detail for my Mk2_withRemoteLoad_1.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt
new file mode 100644
index 0000000..0916a74
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt
@@ -0,0 +1,31 @@
+Detail for my Mk2_withRemoteLoad_2.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt
new file mode 100644
index 0000000..62f30e2
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt
@@ -0,0 +1,38 @@
+Detail for my Mk2_withRemoteLoad_3.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt
new file mode 100644
index 0000000..30f24e0
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt
@@ -0,0 +1,49 @@
+Detail for my Mk2_withRemoteLoad_4.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt
new file mode 100644
index 0000000..3b37255
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt
@@ -0,0 +1,62 @@
+Detail for my Mk2_withRemoteLoad_4a.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+Changes for version _4a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- support for the RF69 RF module has been included.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt
new file mode 100644
index 0000000..b4d00ed
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt
@@ -0,0 +1,67 @@
+Detail for my Mk2_withRemoteLoad_4b.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+Changes for version _4a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- support for the RF69 RF module has been included.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+
+Changes for version _4b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt
new file mode 100644
index 0000000..2eecdef
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt
@@ -0,0 +1,79 @@
+Detail for my Mk2_withRemoteLoad_5.ino sketch
+
+This is an upgraded version of the original Mk2 sketch for use with my
+PCB-based hardware. It still supports two current sensors, CT1 and CT2.
+CT1 is for monitoring the flow of energy at the supply point; CT2 is
+available for monitoring the flow of current to the primary dump-load.
+
+With this version, a secondary dump-load is supported, it being controlled
+by the on-board RF facility. By using the pin-saving hardware option,
+the 4-digit display is still available for use.
+
+The primary (wired) load is still controlled from the "trigger" port. In
+this sketch, the "mode" port is now used for priority selection. If the
+associated switch is open, the local (wired) load has priority. If the
+switch is closed, thereby shorting these pins together, the secondary
+(RF-controlled)load has priority.
+
+The _2 release includes various changes since the _1 version:
+
+- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a
+ leak in the energy bucket, therefore reducing the amount of surplus power
+ that can be diverted. When a negative value is entered, this facility
+ acts like a PV Simulator, which can be very helful for test purposes.
+
+- The state of the energy bucket is displayed at the Serial Monitor every
+ second in Joules.
+
+- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains
+ awake throughout. The library call rf12_sendStart() has been replaced by
+ rf12_sendNow(). These changes are intended to minimise any disruption
+ to the continuous sampling process when RF transmissions are sent.
+
+Changes for version _3:
+
+- The 'long' variable, cycleCount, which counted mains cycles since start-up, has
+ been removed. This variable would have eventually overflowed which could have
+ caused unpredictable effects. The related functionality has been re-implemented
+ using individual 'int' counters.
+
+Changes for version _4:
+
+- a persistence check for the zero-crossing detection has been added. This
+is to remove any false detections of zero-crossings. This effect is seen more
+with some types of transformer than others.
+
+- a mechanism has been added to monitor and display the minimum number
+of sample sets that occur each mains cycle. With a 125us timebase, and three
+ADC samples per set, the expected number of sample sets per 20ms mains cycle
+is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data.
+
+Changes for version _4a:
+
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- support for the RF69 RF module has been included.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+
+Changes for version _4b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+Changes for version _5:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - removal of the unhelpful "triggerNeedsToBeArmed" mechanism
+ - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt b/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt
new file mode 100644
index 0000000..79924a5
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt
@@ -0,0 +1,32 @@
+Detail for my sketch, RST_375us_dev.ino
+
+/* February 2014
+ * Tool to capture the raw V and I samples generated by the Atmega 328P processor
+ * during one or more mains cycles. The data is displayed on the Serial Monitor.
+ *
+ * Voltage samples are displayed as 'v'
+ * Current samples via CT1 are displayed as '1'
+ *
+ * The display is more compact if not every set of samples is shown. This aspect
+ * can be changed at the second of the two lines of code which contain a '%' character.
+ *
+ * February 2021
+ * In the original version, data samples were obtained using the analogRead() function. Now,
+ * they are obtained by the ADC being controlled by a hardware timer with a periodicity of 125 us,
+ * hence a full set of 1 x V and 2 x I samples takes 375 us. The same scheme for collecting
+ * data samples is found in many of my Mk2 PV Router sketches.
+ *
+ * When used with an output stage that has zero-crossing detection, the signal at port D4 can
+ * be used to activate a load for just a single half main cycle. The behaviour of the output signal
+ * from CT1 can then be studied in detail.
+ *
+ * The stream of raw data samples from any floating CT will always be distorted because the CT acts as
+ * a High Pass Filter. This effect is only noticeable when the current that is being measured changes,
+ * such as when an electrical load is turned on or off. This sketch includes additional software which
+ * compensates for this effect. Similar compensation software has been introduced to the varous
+ * "fasterControl" sketches that now exist.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * June 2021
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt
new file mode 100644
index 0000000..392815b
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt
@@ -0,0 +1,25 @@
+/*
+ * Tool to capture the raw samples generated by the Atmega 328P processor
+ * during one or more mains cycles. The data is displayed on the Serial Monitor,
+ * and is also available for subsequent processing using a spreadsheet.
+ *
+ * This version is based on similar code that I posted in December 2012 on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * The pin-allocations have been changed to suit my PCB-based hardware for the
+ * Mk2 PV Router. The integral voltage sensor is fed from one of the secondary
+ * coils of the transformer. Current can be measured via Current Transformers
+ * at the CT1 and CT1 ports.
+ *
+ * Voltage samples are displayed as 'v'
+ * Current samples via CT1 are displayed as '1'
+ * Current samples via CT2 are displayed as '2'
+ *
+ * The display is more compact if not every set of samples is shown. This aspect
+ * can be changed at the second of the two lines which contain a '%' character.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * February 2014
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt
new file mode 100644
index 0000000..1d7c379
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt
@@ -0,0 +1,22 @@
+/*
+ * Tool to capture the raw samples generated by the Atmega 328P processor
+ * during one or more mains cycles. The data is displayed on the Serial Monitor.
+ *
+ * The pin-allocations have been arranged to suit my PCB-based hardware for the
+ * 3-phase Mk2 PV Router. The six analog ports of the Atmega 328 processor are assigned
+ * to the three pairs of AC voltage and current measuring channels.
+ *
+ * The voltage and current waveforms for phases L1, L2 and L3 respectively are denoted
+ * '0' - '5' on the output display.
+ *
+ * The display is more compact if not every set of samples is shown. This aspect
+ * can be changed at the second of the two lines which contain a '%' character.
+ *
+ * Pauses after each set of measurements has been taken. Press 'g', then [cr],
+ * to repeat.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * March 2021
+ */
+
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt b/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt
new file mode 100644
index 0000000..b4d3848
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt
@@ -0,0 +1,26 @@
+/* Transformer_Checker is based on Mk2_RF_datalog_3.ino
+ *
+ * Every 1-phase Mk2 PV Router control board has a mains transformer with two secondary outputs. One output provides
+ * a low-voltage replica of the AC mains voltage; the other is rectified to provide a low-voltage DC supply for the
+ * processor. Although the power consumption of the Atmel 328P processor is fairly constant, it will be increase
+ * whenever the output stage is activated. The increased draw from the DC supply will cause the amplitude of the AC signal
+ * from the other output to slightly decrease.
+ *
+ * This sketch can be used to quantify the above effect. A standard output stage should be connected to the primary
+ * output port but no AC load should be connected otherwise a consequent reduction in the local mains voltage
+ * could adversely affect this test.
+ *
+ * Via the Serial Monitor, this sketch will display the percentage reduction in the measured Vrms value whenever
+ * the output stage is activated. By adding an extra LED which operates in anti-phase with the primary output, the
+ * reduction in Vrms can be effectively eliminated. Both LEDs can be driven by the same output port but with their other
+ * terminals connected to opposite power rails via series resistors of appropriate values.
+ *
+ * Any reduction in the measured Vrms value when the output stage is activated represents a non-linearity which will
+ * result in less than ideal performance. By means of this sketch, an extra LED and series resistor can be used to
+ * minimise any such effect.
+ *
+ * July 2021: first release.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt
new file mode 100644
index 0000000..c5b94a8
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt
@@ -0,0 +1,17 @@
+Detail for my cal_CT1_v_meter.ino sketch
+
+/* cal_CT1_v_meter.ino
+ *
+ * February 2018
+ * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to
+ * mimic the behaviour of a digital electricity meter.
+ *
+ * CT1 should be clipped around one of the live cables that pass through the
+ * meter. The energy flow measured by CT1 is noted and a short pulse is generated
+ * whenever a pre-set amount of energy has been recorded (normally 3600J).
+ *
+ * This stream of pulses can then be compared against optical pulses from a standard
+ * electrical utility meter. The pulse rate can be varied by adjusting the value
+ * of powerCal_grid. When the two streams of pulses are in synch, correct calibration
+ * of the CT1 channel has been achieved.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt
new file mode 100644
index 0000000..37ed326
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt
@@ -0,0 +1,23 @@
+Detail for my cal_CT2_v_CT1.ino sketch
+
+/* cal_CT2_v_CT1.ino
+ *
+ * February 2018
+ * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to
+ * mimic the behaviour of a dual channel electricity meter. The sensitivity of the
+ * CT2 channel can then be adjusted to match that of the CT1 channel. Before using
+ * this sketch, the CT1 channel would normally have been calibrated against the
+ * user's electricity meter.
+ *
+ * CT1 and CT2 should be fitted around the same current-carrying conductor. If
+ * CT2 has been built into a completed system, the bypass switch can be used to force
+ * power down that path.
+ *
+ * The energy flow on each channel is noted and a short pulse is generated whenever a
+ * pre-set amount of energy has been recorded (normally 3600J). The two streams of
+ * pulses can then be compared. The pulse rate for the CT2 channel can be varied by
+ * adjusting the value of powerCal_diverted. When the two streams of pulses are in
+ * synch, correct calibration of the CT2 channel has been achieved.
+ *
+ * The two pulse streams can be synchronised at any time by earthing R11 which is
+ * tracked to port A0 (aka D14).
diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt
new file mode 100644
index 0000000..25dfb06
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt
@@ -0,0 +1,48 @@
+Detail for my cal_bothDisplays_3.ino sketch
+
+/* cal_bothDisplays_3.ino
+ *
+ * This sketch provides an easy way of calibrating the current sensors that are
+ * facilitated via the CT1 and CT2 ports. Channel selection is provided by a
+ * switch at the "mode" connector (digital IO port D3) on the control board.
+ * The default selection is CT1; CT2 is selected when the switch is closed.
+ * The measured value is shown on the 4-digit display, and also at the Serial Monitor.
+ *
+ * CT1 normally monitors the power at the grid supply point.
+ * CT2 normaly monitors power that is sent to the dump load(s).
+ *
+ * For this test, the selected CT should be clipped around a lead through which a known
+ * amount of power is flowing. This can be compared against the displayed value
+ * which is proportional to the powerCal setting. Once the optimal powerCal values
+ * have been obtained for each channel, these values can be transferred into the
+ * main Mk2 PV Router sketch.
+ *
+ * Depending on which way around the CT is connected, the measured value may be
+ * positive or negative. If it is negative, the display will either flash or
+ * display a negative symbol. Its behaviour will depend on the way that the display
+ * has been configured.
+ *
+ * The 4-digit display can be driven in two different ways, one with an extra pair
+ * of logic chips, and one without. The appropriate version of the sketch must be
+ * selected by including or commenting out the "#define PIN_SAVING_HARDWARE"
+ * statement near the top of the code.
+ *
+ * With the pin-saving logic, the display is not able to show a '-' symbol. But
+ * in the alternative mode, it is.
+ *
+ * December 2017, upgraded to cal_bothDisplays_2:
+ * In the original version, the mains cycle counter cycled through the values 0 to
+ * CYCLES_PER_SECOND inclusive so data was only processed every (CYCLES_PER_SECOND + 1)
+ * mains cycles. Because the accumulated energy data was divided by CYCLES_PER_SECOND,
+ * there was an error of approx 2% in the displayed power value. In version 2,
+ * the logic has been corrected to avoid this error.
+ *
+ * June 2022, upgraded to cal_bothDisplays_3:
+ * The 4-digit display still shows the power that is measured on the selected CT channel. The
+ * Serial monitor however now shows the average power at both CT channels. Previously, the value
+ * for the selected channel was shown twice.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * June 2022
+ */
diff --git a/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt b/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt
new file mode 100644
index 0000000..b5d4fca
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt
@@ -0,0 +1,9 @@
+Detail for my demo_bothDisplays.ino sketch
+
+This sketch comprises just the display section of my Mk2 PV Router code.
+Its intended purpose was to provide an easy way of checking the operation
+of the 4-digit display, but the later sketch segCheck_bothDisplays.ino
+is more suitable for this task.
+
+This original version is mentioned in the Build Guide so can remain here
+for completeness.
\ No newline at end of file
diff --git a/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt b/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt
new file mode 100644
index 0000000..745a77d
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt
@@ -0,0 +1,4 @@
+Notes re. the original circuit diagram for my (orange) 'main' PCB @ rev 1.1
+
+The original circuit diagram has R1 = 10K which is outside the
+specified range for the Atmega 328P processor.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt
new file mode 100644
index 0000000..b66cfd2
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt
@@ -0,0 +1,7 @@
+Notes re. my main_rev1_circuit_2 diagram
+
+This diagram is for the original (orange) PCB @ rev 1.1
+
+The diagram was updated in October 2015 to show the value of R1
+being increased from 10K to 47K. This change is in-line with
+the published specification for the Atmega 328P processor.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt
new file mode 100644
index 0000000..ae79e5a
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt
@@ -0,0 +1,7 @@
+Notes re. my main_rev2_circuit_2 diagram
+
+This diagram is for the later (green) PCB @ rev 2.1
+
+The diagram was updated in October 2015 to show the value of R1
+being increased from 10K to 47K. This change is in-line with
+the published specification for the Atmega 328P processor.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt
new file mode 100644
index 0000000..97e8261
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt
@@ -0,0 +1,5 @@
+Notes re. my main_rev4.1_circuit diagram
+
+This diagram is for the latest (black) 1-phase control board @ rev 4.1
+
+There are no known problems with this board or its circuit digram
diff --git a/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt b/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt
new file mode 100644
index 0000000..0c2f721
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt
@@ -0,0 +1,52 @@
+Detail for my sketch remoteUnit_fasterControl_1.ino
+
+/* remoteUnit_fasterControl_1.ino
+ *
+ * This sketch is to control a remote load for a Mk2 PV Router at the receiver end
+ * of an RF link. If RF transmission is lost, the load is turned off. A repeater
+ * signal is available at the 'mode' connector. This is intended to drive an LED
+ * with an appropriate series resistor, e.g. 120R.
+ *
+ * The ability to measure and display the amount of energy which has been diverted
+ * via the remote load is included. For this to happen, one of the live cores
+ * needs to pass through a CT which connects to the 'CT2' connector.
+ *
+ * The 'CT1' connector has been re-used in this sketch to provide a 2-colour
+ * indication of the state of the RF link. A schematic for this circuit may be
+ * found immediately below this header.
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the
+ * OpenEnergyMonitor forum. That version, and other related material, can be
+ * found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to
+ * remove a timing uncertainty. Support for the RF69 RF module has also been included.
+ *
+ * January 2016: updated to remote_Mk2_receiver_1b:
+ * The variables to store the ADC results are now declared as "volatile" to remove
+ * any possibility of incorrect operation due to optimisation by the compiler.
+ *
+ * February 2016: updated to remote_Mk2_receiver_2, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - change all instances of "triac" to "load"
+ *
+ * September 2022: updated to remoteUnit_fasterConrol_1, with this change:
+ * - RF payload reduced to just one integer for the load state. For use with the transmitter
+ * sketch, Mk2_fasterControl_withRemoteLoad_n
+ * - the hardware timer that controls the ADC has been increased from 200 to 250 us (just to
+ * reduce the workload).
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt
new file mode 100644
index 0000000..cd6bbd2
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt
@@ -0,0 +1,21 @@
+Detail for my remote_Mk2_receiver_1.ino sketch
+
+This sketch is for use as the receiver part of a Mk2 PV Router system
+that is operated via an RF link rather than via a control cable. It is
+intended for use with my PCB-based hardware.
+
+One current sensor is supported at the CT2 port. This can be used for
+monitoring the power that is diverted by the receiver unit. The
+diverted energy total in kWh is available at the 4-digit display connector.
+The display returns to its idle state after a period of 10 hours during
+which the control signal has never been in the 'on' state.
+
+The CT1 port has been re-allocated for driving a pair of LEDs to show
+the status of the RF control link. The circuit for this feature is shown
+in the header of the sketch.
+
+The output signal for controlling the load, as instructed by the
+associated transmitter, is available at the "mode" port (D3) in active-high
+format and at the "trigger" port (D4) in active-low format.
+
+The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino
diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt
new file mode 100644
index 0000000..8d67f7b
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt
@@ -0,0 +1,30 @@
+Detail for my remote_Mk2_receiver_1a.ino sketch
+
+This sketch is for use as the receiver part of a Mk2 PV Router system
+that is operated via an RF link rather than via a control cable. It is
+intended for use with my PCB-based hardware.
+
+One current sensor is supported at the CT2 port. This can be used for
+monitoring the power that is diverted by the receiver unit. The
+diverted energy total in kWh is available at the 4-digit display connector.
+The display returns to its idle state after a period of 10 hours during
+which the control signal has never been in the 'on' state.
+
+The CT1 port has been re-allocated for driving a pair of LEDs to show
+the status of the RF control link. The circuit for this feature is shown
+in the header of the sketch.
+
+The output signal for controlling the load, as instructed by the
+associated transmitter, is available at the "mode" port (D3) in active-high
+format and at the "trigger" port (D4) in active-low format.
+
+The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino
+
+Changes for version _1a:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt
new file mode 100644
index 0000000..72dc79c
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt
@@ -0,0 +1,35 @@
+Detail for my remote_Mk2_receiver_1b.ino sketch
+
+This sketch is for use as the receiver part of a Mk2 PV Router system
+that is operated via an RF link rather than via a control cable. It is
+intended for use with my PCB-based hardware.
+
+One current sensor is supported at the CT2 port. This can be used for
+monitoring the power that is diverted by the receiver unit. The
+diverted energy total in kWh is available at the 4-digit display connector.
+The display returns to its idle state after a period of 10 hours during
+which the control signal has never been in the 'on' state.
+
+The CT1 port has been re-allocated for driving a pair of LEDs to show
+the status of the RF control link. The circuit for this feature is shown
+in the header of the sketch.
+
+The output signal for controlling the load, as instructed by the
+associated transmitter, is available at the "mode" port (D3) in active-high
+format and at the "trigger" port (D4) in active-low format.
+
+The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino
+
+Changes for version _1a:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _1b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt
new file mode 100644
index 0000000..2f7b902
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt
@@ -0,0 +1,47 @@
+Detail for my remote_Mk2_receiver_2.ino sketch
+
+This sketch is for use as the receiver part of a Mk2 PV Router system
+that is operated via an RF link rather than via a control cable. It is
+intended for use with my PCB-based hardware.
+
+One current sensor is supported at the CT2 port. This can be used for
+monitoring the power that is diverted by the receiver unit. The
+diverted energy total in kWh is available at the 4-digit display connector.
+The display returns to its idle state after a period of 10 hours during
+which the control signal has never been in the 'on' state.
+
+The CT1 port has been re-allocated for driving a pair of LEDs to show
+the status of the RF control link. The circuit for this feature is shown
+in the header of the sketch.
+
+The output signal for controlling the load, as instructed by the
+associated transmitter, is available at the "mode" port (D3) in active-high
+format and at the "trigger" port (D4) in active-low format.
+
+The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino
+
+Changes for version _1a:
+- A minor change has been made to the function timerIsr() so as to resolve
+a timing anomaly that has previously existed. With the new arrangement, a
+complete set of data samples is made available by the ISR for use by the
+main code. There is no longer any possibility of these values being
+overwritten before they are processed.
+
+- the display timeout period has been reduced to 8 hours instead of 10.
+
+Changes for version _1b:
+- The variables to store ADC data are now declared as "volatile" to remove
+any possibility of incorrect operation due to optimisation by the compiler.
+
+Changes for version _2:
+ January 2016: updated to remote_Mk2_receiver_2, with these changes:
+ - improvements to the start-up logic. The start of normal operation is now
+ synchronised with the start of a new mains cycle.
+ - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ from the Vsample stream. This resolves an anomaly which has been present since
+ the start of this project. Although the amount of feedback has previously been
+ excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ - change all instances of "triac" to "load"
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino
new file mode 100644
index 0000000..41b134c
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino
@@ -0,0 +1,852 @@
+/* Mk2_3phase_RFdatalog_1
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order.
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * January 2015
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+//#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define TRIAC_ON LOW
+#define TRIAC_OFF HIGH
+#define DATALOG_PERIOD_IN_SECONDS 2
+
+const byte noOfDumploads = 3; // The logic expects a minimum of 2 dumploads,
+ // for local & remote loads, but neither has to
+ // be physically present.
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {SWITCHED_PRIORITIES, NORMAL_PRIORITIES};
+
+enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm
+enum energyStates energyStateNow;
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+const byte physicalLoad_0_pin = 8; // for 3-phase PCB, Load #1
+const byte physicalLoad_1_pin = 7; // for 3-phase PCB, Load #2
+const byte physicalLoad_2_pin = 6; // for 3-phase PCB, Load #3
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode
+const int postMidPointCrossingDelayForAF_cycles = 10; // in 20 ms counts
+const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts
+byte activeLoadID; // only one load may operate freely at a time.
+int mainsCyclesSinceLastMidPointCrossing = 0;
+int mainsCyclesSinceLastChangeOfLoadState = 0;
+float energyStateAtLastTransition;
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+int sampleV[NO_OF_PHASES];
+int sampleI[NO_OF_PHASES];
+
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.0435, 0.043, 0.043};
+//const float powerCal[NO_OF_PHASES] = {0.05};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch!
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+float energyBucketCapacity_main;
+float energyBucketCapacity_perPhase;
+float energyInBucket_main;
+float midPointOfMainEnergyBucket;
+boolean energyLevelInUpperHalf;
+
+// per-phase items
+float energyStateOfPhase[NO_OF_PHASES]; // an energy bucket per phase
+float energyBucketLevel_perPhase_max; // for restraining the per-phase energy bucket levels
+float energyBucketLevel_perPhase_min; // for restraining the per-phase energy bucket levels
+
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_1.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ physicalLoadState[i] = LOAD_OFF;
+ }
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ energyBucketCapacity_main = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfMainEnergyBucket = energyBucketCapacity_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+ energyStateAtLastTransition = midPointOfMainEnergyBucket;
+
+ // for the per-phase energy buckets
+ energyBucketCapacity_perPhase = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND;
+ energyBucketLevel_perPhase_max = energyBucketCapacity_perPhase * 0.5;
+ energyBucketLevel_perPhase_min = energyBucketCapacity_perPhase * -0.5;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as data is made available by the ADC, the main processor can start to work
+// on it immediately.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThismainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+ float realPower;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ /* This is the start of a new +ve half cycle, for this phase, just after the
+ * zero-crossing point. Before the contribution from this phase can be added
+ * to the running total, the cal factor for this phase must be applied.
+ */
+ realPower = (float)(sumP[phase] / samplesDuringThismainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ sumP[phase] = 0;
+ samplesDuringThismainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if ((phase == 0) && samplesDuringThismainsCycle[0] == 5)
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+// cycleCount++;
+ mainsCyclesSinceLastMidPointCrossing++;
+ mainsCyclesSinceLastChangeOfLoadState++;
+ datalogCountInMainsCycles++;
+
+ /* Now it's time to determine whether any of the the loads need to be changed.
+ * This is a 2-stage process:
+ * First, change the LOGICAL loads as necessary, then update the PHYSICAL
+ * loads according to the mapping that exists between them. The mapping is
+ * 1:1 by default but can be altered by a hardware switch which allows the
+ * priorities of the first two load to be swapped.
+ * This code uses a single-threshold algorithm which relies on regular switching
+ * of the load to maintain the energy balance within the permitted range.
+ */
+
+ // when using the single-threshold power diversion algorithm, a counter needs
+ // to be reset whenever the energy level in the accumulator crosses the mid-point
+ //
+ enum energyStates energyStateOnLastLoop = energyStateNow;
+
+ if (energyInBucket_main > midPointOfMainEnergyBucket) {
+ energyStateNow = UPPER_HALF; }
+ else {
+ energyStateNow = LOWER_HALF; }
+
+ if (energyStateNow != energyStateOnLastLoop) {
+ mainsCyclesSinceLastMidPointCrossing = 0; }
+
+
+ if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles)
+ {
+ if (energyStateNow == UPPER_HALF)
+ {
+ increaseLoadIfPossible(); // to reduce the level in the bucket
+ }
+ else
+ {
+ decreaseLoadIfPossible(); // to increase the level in the bucket
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update all the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod));
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+//
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+ //
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for the phase that is
+ // being processed. This needs to be done right from the start.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ checkLoadPrioritySelection(); // updates load priorities if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negitive
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when beyondStartUpPhase is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThismainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * 50
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+//
+ // Apply max and min limits to the main accumulator's level
+ if (energyInBucket_main > energyBucketCapacity_main) {
+ energyInBucket_main = energyBucketCapacity_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+}
+
+void increaseLoadIfPossible()
+{
+ /* if permitted by A/F rules, turn on the highest priority logical load that is not already on.
+ */
+ boolean changed = false;
+
+ // Only one load may operate freely at a time. Other loads are prevented from
+ // switching until a sufficient period has elapsed since the last transition, and
+ // then only if the energy level has not have fallen since the previous transition.
+ // These measures allow a lower priority load to contribute if a higher priority
+ // load is not having the desired effect, but not immediately.
+ //
+ if (energyInBucket_main >= energyStateAtLastTransition)
+ {
+// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles);
+ boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles);
+ for (int i = 0; i < noOfDumploads && !changed; i++)
+ {
+ if (logicalLoadState[i] == LOAD_OFF)
+ {
+ if ((i == activeLoadID) || timeout)
+ {
+ logicalLoadState[i] = LOAD_ON;
+ mainsCyclesSinceLastChangeOfLoadState = 0;
+ energyStateAtLastTransition = energyInBucket_main;
+ activeLoadID = i;
+ changed = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // energy level has not risen so there's no need to apply any more load
+ }
+}
+
+void decreaseLoadIfPossible()
+{
+ /* if permitted by A/F rules, turn off the highest priority logical load that is not already off.
+ */
+ boolean changed = false;
+
+ // Only one load may operate freely at a time. Other loads are prevented from
+ // switching until a sufficient period has elapsed since the last transition, and
+ // then only if the energy level has not have risen since the previous transition.
+ // These measures allow a lower priority load to contribute if a higher priority
+ // load is not having the desired effect, but not immediately.
+ //
+ if (energyInBucket_main <= energyStateAtLastTransition)
+ {
+// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles);
+ boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles);
+// for (int i = 0; i < noOfDumploads && !done; i++)
+ for (int i = (noOfDumploads -1); i >= 0 && !changed; i--)
+ {
+ if (logicalLoadState[i] == LOAD_ON)
+ {
+ if ((i == activeLoadID) || timeout)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ mainsCyclesSinceLastChangeOfLoadState = 0;
+ energyStateAtLastTransition = energyInBucket_main;
+ activeLoadID = i;
+ changed = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // energy level has not fallen so there's no need to apply any more load
+ }
+}
+
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * By default, the association between the physical and logical loads is 1:1. If
+ * physical load 1 is set to have priority, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == SWITCHED_PRIORITIES)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == NORMAL_PRIORITIES) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles;
+ }
+ else
+ {
+ postMidPointCrossingDelay_cycles = 0;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" energyBucketCapacity_main = ");
+ Serial.println(energyBucketCapacity_main);
+ Serial.print(" postMidPointCrossingDelay_cycles = ");
+ Serial.println(postMidPointCrossingDelay_cycles);
+ Serial.print(" interLoadSeparationDelay_cycles = ");
+ Serial.println(interLoadSeparationDelay_cycles);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino
new file mode 100644
index 0000000..5b823f8
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino
@@ -0,0 +1,875 @@
+/* Mk2_3phase_RFdatalog_1 -> Mk2_3phase_RFdatalog_1a
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order.
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * 14-Aug-2015 renamed as version 1a
+ * - addition of mechanism to display the minimum number of sample sets per mains cycle;
+ * - to avoid data loss, datalog messages are no longer set to the Serial port.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * August 2015
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+//#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define TRIAC_ON LOW
+#define TRIAC_OFF HIGH
+#define DATALOG_PERIOD_IN_SECONDS 2
+
+const byte noOfDumploads = 3; // The logic expects a minimum of 2 dumploads,
+ // for local & remote loads, but neither has to
+ // be physically present.
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {SWITCHED_PRIORITIES, NORMAL_PRIORITIES};
+
+enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm
+enum energyStates energyStateNow;
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+const byte physicalLoad_0_pin = 8; // for 3-phase PCB, Load #1
+const byte physicalLoad_1_pin = 7; // for 3-phase PCB, Load #2
+const byte physicalLoad_2_pin = 6; // for 3-phase PCB, Load #3
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode
+const int postMidPointCrossingDelayForAF_cycles = 10; // in 20 ms counts
+const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts
+byte activeLoadID; // only one load may operate freely at a time.
+int mainsCyclesSinceLastMidPointCrossing = 0;
+int mainsCyclesSinceLastChangeOfLoadState = 0;
+float energyStateAtLastTransition;
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+int sampleV[NO_OF_PHASES];
+int sampleI[NO_OF_PHASES];
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int sampleCount_forContinuityChecker;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.0435, 0.043, 0.043};
+//const float powerCal[NO_OF_PHASES] = {0.05};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch!
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+float energyBucketCapacity_main;
+float energyBucketCapacity_perPhase;
+float energyInBucket_main;
+float midPointOfMainEnergyBucket;
+boolean energyLevelInUpperHalf;
+
+// per-phase items
+float energyStateOfPhase[NO_OF_PHASES]; // an energy bucket per phase
+float energyBucketLevel_perPhase_max; // for restraining the per-phase energy bucket levels
+float energyBucketLevel_perPhase_min; // for restraining the per-phase energy bucket levels
+
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_1.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ physicalLoadState[i] = LOAD_OFF;
+ }
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ energyBucketCapacity_main = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfMainEnergyBucket = energyBucketCapacity_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+ energyStateAtLastTransition = midPointOfMainEnergyBucket;
+
+ // for the per-phase energy buckets
+ energyBucketCapacity_perPhase = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND;
+ energyBucketLevel_perPhase_max = energyBucketCapacity_perPhase * 0.5;
+ energyBucketLevel_perPhase_min = energyBucketCapacity_perPhase * -0.5;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as data is made available by the ADC, the main processor can start to work
+// on it immediately.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThismainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+ float realPower;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ /* This is the start of a new +ve half cycle, for this phase, just after the
+ * zero-crossing point. Before the contribution from this phase can be added
+ * to the running total, the cal factor for this phase must be applied.
+ */
+ realPower = (float)(sumP[phase] / samplesDuringThismainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ if (phase == 0)
+ {
+ // continuity checker
+ if (samplesDuringThismainsCycle[0] < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = samplesDuringThismainsCycle[0]; }
+
+ sampleCount_forContinuityChecker++;
+ if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ sampleCount_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+ }
+
+ sumP[phase] = 0;
+ samplesDuringThismainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if ((phase == 0) && samplesDuringThismainsCycle[0] == 5)
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+// cycleCount++;
+ mainsCyclesSinceLastMidPointCrossing++;
+ mainsCyclesSinceLastChangeOfLoadState++;
+ datalogCountInMainsCycles++;
+
+ /* Now it's time to determine whether any of the the loads need to be changed.
+ * This is a 2-stage process:
+ * First, change the LOGICAL loads as necessary, then update the PHYSICAL
+ * loads according to the mapping that exists between them. The mapping is
+ * 1:1 by default but can be altered by a hardware switch which allows the
+ * priorities of the first two load to be swapped.
+ * This code uses a single-threshold algorithm which relies on regular switching
+ * of the load to maintain the energy balance within the permitted range.
+ */
+
+ // when using the single-threshold power diversion algorithm, a counter needs
+ // to be reset whenever the energy level in the accumulator crosses the mid-point
+ //
+ enum energyStates energyStateOnLastLoop = energyStateNow;
+
+ if (energyInBucket_main > midPointOfMainEnergyBucket) {
+ energyStateNow = UPPER_HALF; }
+ else {
+ energyStateNow = LOWER_HALF; }
+
+ if (energyStateNow != energyStateOnLastLoop) {
+ mainsCyclesSinceLastMidPointCrossing = 0; }
+
+
+ if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles)
+ {
+ if (energyStateNow == UPPER_HALF)
+ {
+ increaseLoadIfPossible(); // to reduce the level in the bucket
+ }
+ else
+ {
+ decreaseLoadIfPossible(); // to increase the level in the bucket
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update all the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod));
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+/*
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+ */
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for the phase that is
+ // being processed. This needs to be done right from the start.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ checkLoadPrioritySelection(); // updates load priorities if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negitive
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when beyondStartUpPhase is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThismainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * 50
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+//
+ // Apply max and min limits to the main accumulator's level
+ if (energyInBucket_main > energyBucketCapacity_main) {
+ energyInBucket_main = energyBucketCapacity_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+}
+
+void increaseLoadIfPossible()
+{
+ /* if permitted by A/F rules, turn on the highest priority logical load that is not already on.
+ */
+ boolean changed = false;
+
+ // Only one load may operate freely at a time. Other loads are prevented from
+ // switching until a sufficient period has elapsed since the last transition, and
+ // then only if the energy level has not have fallen since the previous transition.
+ // These measures allow a lower priority load to contribute if a higher priority
+ // load is not having the desired effect, but not immediately.
+ //
+ if (energyInBucket_main >= energyStateAtLastTransition)
+ {
+// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles);
+ boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles);
+ for (int i = 0; i < noOfDumploads && !changed; i++)
+ {
+ if (logicalLoadState[i] == LOAD_OFF)
+ {
+ if ((i == activeLoadID) || timeout)
+ {
+ logicalLoadState[i] = LOAD_ON;
+ mainsCyclesSinceLastChangeOfLoadState = 0;
+ energyStateAtLastTransition = energyInBucket_main;
+ activeLoadID = i;
+ changed = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // energy level has not risen so there's no need to apply any more load
+ }
+}
+
+void decreaseLoadIfPossible()
+{
+ /* if permitted by A/F rules, turn off the highest priority logical load that is not already off.
+ */
+ boolean changed = false;
+
+ // Only one load may operate freely at a time. Other loads are prevented from
+ // switching until a sufficient period has elapsed since the last transition, and
+ // then only if the energy level has not have risen since the previous transition.
+ // These measures allow a lower priority load to contribute if a higher priority
+ // load is not having the desired effect, but not immediately.
+ //
+ if (energyInBucket_main <= energyStateAtLastTransition)
+ {
+// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles);
+ boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles);
+// for (int i = 0; i < noOfDumploads && !done; i++)
+ for (int i = (noOfDumploads -1); i >= 0 && !changed; i--)
+ {
+ if (logicalLoadState[i] == LOAD_ON)
+ {
+ if ((i == activeLoadID) || timeout)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ mainsCyclesSinceLastChangeOfLoadState = 0;
+ energyStateAtLastTransition = energyInBucket_main;
+ activeLoadID = i;
+ changed = true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // energy level has not fallen so there's no need to apply any more load
+ }
+}
+
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * By default, the association between the physical and logical loads is 1:1. If
+ * physical load 1 is set to have priority, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == SWITCHED_PRIORITIES)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == NORMAL_PRIORITIES) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles;
+ }
+ else
+ {
+ postMidPointCrossingDelay_cycles = 0;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" energyBucketCapacity_main = ");
+ Serial.println(energyBucketCapacity_main);
+ Serial.print(" postMidPointCrossingDelay_cycles = ");
+ Serial.println(postMidPointCrossingDelay_cycles);
+ Serial.print(" interLoadSeparationDelay_cycles = ");
+ Serial.println(interLoadSeparationDelay_cycles);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino
new file mode 100644
index 0000000..720d335
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino
@@ -0,0 +1,941 @@
+/* Mk2_3phase_RFdatalog_2.ino
+ *
+ * Issue 1 was released in January 2015.
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order.
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * Jan 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes:
+ * - Improved control of multiple loads has been imported from the
+ * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino
+ * - the ISR has been upgraded to fix a possible timing anomaly
+ * - variables to store ADC samples are now declared as "volatile"
+ * - for RF69 RF module is now supported
+ * - a performance check has been added with the result being sent to the Serial port
+ * - control signals for loads are now active-high to suit the latest 3-phase PCB
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+//#define RF69_COMPAT 0 // for the RFM12B
+#define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define WORKING_ZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define DATALOG_PERIOD_IN_SECONDS 5
+
+const byte noOfDumploads = 3;
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB)
+enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB)
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+// D4 is not in use
+const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB)
+const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB)
+const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB)
+// D8 is not in use
+// D9 is not in use
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, some variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+// for 3-phase use, with units of Joules * CYCLES_PER_SECOND
+float capacityOfEnergyBucket_main;
+float energyInBucket_main;
+float midPointOfEnergyBucket_main;
+float lowerThreshold_default;
+float lowerEnergyThreshold;
+float upperThreshold_default;
+float upperEnergyThreshold;
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4
+
+// for improved control of multiple loads
+boolean recentTransition = false;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only
+byte activeLoad = 0;
+
+// for datalogging
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+volatile int sampleV[NO_OF_PHASES];
+volatile int sampleI[NO_OF_PHASES];
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int mainsCycles_forContinuityChecker;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch.
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_2.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ }
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being synchnonised only via the dataReady flag.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThisMainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle, for this phase, just after the
+ // zero-crossing point. Before the contribution from this phase can be added
+ // to the running total, the cal factor for this phase must be applied.
+ //
+ float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ // A performance check to monitor and display the minimum number of sets of
+ // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05
+ //
+ if (phase == 0)
+ {
+ if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle)
+ {
+ lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase];
+ }
+ mainsCycles_forContinuityChecker++;
+ if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ mainsCycles_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+ }
+
+ sumP[phase] = 0;
+ samplesDuringThisMainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+ datalogCountInMainsCycles++;
+
+ // Changling the state of the loads is is a 3-part process:
+ // - change the LOGICAL load states as necessary to maintain the energy level
+ // - update the PHYSICAL load states according to the logical -> physical mapping
+ // - update the driver lines for each of the loads.
+ //
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_main > midPointOfEnergyBucket_main)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ upperEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_main)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_main;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('+');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ lowerEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('-');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update the control ports for each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_main > capacityOfEnergyBucket_main) {
+ energyInBucket_main = capacityOfEnergyBucket_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod));
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+//
+ Serial.print(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+*/
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+
+
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ Serial.print(logicalLoadState[i]);
+ }
+ Serial.println();
+*/
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ mainsCycles_forContinuityChecker = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for the phase that is
+ // being processed. This needs to be done right from the start.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ if (phase == 0)
+ {
+ checkLoadPrioritySelection(); // updates load priorities if the switch is changed
+ }
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negitive
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when the beyondStartUpPhase flag is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThisMainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+
+ // Applying max and min limits to the main accumulator's level
+ // is deferred until after the energy related decisions have been taken
+ //
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // NB. the index cannot be a 'byte' because the loop would not terminate correctly!
+ for (char index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_main = ");
+ Serial.println(capacityOfEnergyBucket_main);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino
new file mode 100644
index 0000000..d80279f
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino
@@ -0,0 +1,966 @@
+/* Mk2_3phase_RFdatalog_3.ino
+ *
+ * Issue 1 was released in January 2015.
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order. A suitable
+ * output-stage is required for each load; this can be either triac-based, or a
+ * Solid State Relay.
+ *
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes:
+ * - Improved control of multiple loads has been imported from the
+ * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino
+ * - the ISR has been upgraded to fix a possible timing anomaly
+ * - variables to store ADC samples are now declared as "volatile"
+ * - for RF69 RF module is now supported
+ * - a performance check has been added with the result being sent to the Serial port
+ * - control signals for loads are now active-high to suit the latest 3-phase PCB
+ *
+ * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - The reported power at each of the phases has been inverted. These values are now in
+ * line with the Open Energy Monitor convention, whereby import is positive and
+ * export is negative.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define WORKING_ZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define DATALOG_PERIOD_IN_SECONDS 5
+
+const byte noOfDumploads = 3;
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB)
+enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB)
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+// D4 is not in use
+const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB)
+const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB)
+const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB)
+// D8 is not in use
+// D9 is not in use
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, some variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+// for 3-phase use, with units of Joules * CYCLES_PER_SECOND
+float capacityOfEnergyBucket_main;
+float energyInBucket_main;
+float midPointOfEnergyBucket_main;
+float lowerThreshold_default;
+float lowerEnergyThreshold;
+float upperThreshold_default;
+float upperEnergyThreshold;
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4
+
+// for improved control of multiple loads
+boolean recentTransition = false;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only
+byte activeLoad = 0;
+
+// for datalogging
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+volatile int sampleV[NO_OF_PHASES];
+volatile int sampleI[NO_OF_PHASES];
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int mainsCycles_forContinuityChecker;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch.
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_3.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ }
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being synchnonised only via the dataReady flag.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThisMainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This is the start of a new +ve half cycle, for this phase, just after the
+ // zero-crossing point. Before the contribution from this phase can be added
+ // to the running total, the cal factor for this phase must be applied.
+ //
+ float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ // A performance check to monitor and display the minimum number of sets of
+ // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05
+ //
+ if (phase == 0)
+ {
+ if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle)
+ {
+ lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase];
+ }
+ mainsCycles_forContinuityChecker++;
+ if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ mainsCycles_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+ }
+
+ sumP[phase] = 0;
+ samplesDuringThisMainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+ datalogCountInMainsCycles++;
+
+ // Changling the state of the loads is is a 3-part process:
+ // - change the LOGICAL load states as necessary to maintain the energy level
+ // - update the PHYSICAL load states according to the logical -> physical mapping
+ // - update the driver lines for each of the loads.
+ //
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_main > midPointOfEnergyBucket_main)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ upperEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_main)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_main;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('+');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ lowerEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('-');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update the control ports for each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_main > capacityOfEnergyBucket_main) {
+ energyInBucket_main = capacityOfEnergyBucket_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod));
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+//
+ Serial.print(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+*/
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+
+
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ Serial.print(logicalLoadState[i]);
+ }
+ Serial.println();
+*/
+ }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for removing the DC
+ // component from the phase that is being processed.
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12);
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ if (phase == 0)
+ {
+ checkLoadPrioritySelection(); // updates load priorities if the switch is changed
+ }
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when the beyondStartUpPhase flag is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThisMainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+
+ // Applying max and min limits to the main accumulator's level
+ // is deferred until after the energy related decisions have been taken
+ //
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // NB. the index cannot be a 'byte' because the loop would not terminate correctly!
+ for (char index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_main = ");
+ Serial.println(capacityOfEnergyBucket_main);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino
new file mode 100644
index 0000000..29a7b98
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino
@@ -0,0 +1,964 @@
+/* Mk2_3phase_RFdatalog_3a.ino
+ *
+ * Issue 1 was released in January 2015.
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order. A suitable
+ * output-stage is required for each load; this can be either triac-based, or a
+ * Solid State Relay.
+ *
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes:
+ * - Improved control of multiple loads has been imported from the
+ * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino
+ * - the ISR has been upgraded to fix a possible timing anomaly
+ * - variables to store ADC samples are now declared as "volatile"
+ * - for RF69 RF module is now supported
+ * - a performance check has been added with the result being sent to the Serial port
+ * - control signals for loads are now active-high to suit the latest 3-phase PCB
+ *
+ * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - The reported power at each of the phases has been inverted. These values are now in
+ * line with the Open Energy Monitor convention, whereby import is positive and
+ * export is negative.
+ *
+ * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define WORKING_ZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define DATALOG_PERIOD_IN_SECONDS 5
+
+const byte noOfDumploads = 3;
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB)
+enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB)
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+// D4 is not in use
+const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB)
+const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB)
+const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB)
+// D8 is not in use
+// D9 is not in use
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, some variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+// for 3-phase use, with units of Joules * CYCLES_PER_SECOND
+float capacityOfEnergyBucket_main;
+float energyInBucket_main;
+float midPointOfEnergyBucket_main;
+float lowerThreshold_default;
+float lowerEnergyThreshold;
+float upperThreshold_default;
+float upperEnergyThreshold;
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4
+
+// for improved control of multiple loads
+boolean recentTransition = false;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only
+byte activeLoad = 0;
+
+// for datalogging
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+volatile int sampleV[NO_OF_PHASES];
+volatile int sampleI[NO_OF_PHASES];
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int mainsCycles_forContinuityChecker;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch.
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_3a.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ }
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being synchnonised only via the dataReady flag.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThisMainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This is the start of a new +ve half cycle, for this phase, just after the
+ // zero-crossing point. Before the contribution from this phase can be added
+ // to the running total, the cal factor for this phase must be applied.
+ //
+ float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ // A performance check to monitor and display the minimum number of sets of
+ // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05
+ //
+ if (phase == 0)
+ {
+ if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle)
+ {
+ lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase];
+ }
+ mainsCycles_forContinuityChecker++;
+ if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ mainsCycles_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+ }
+
+ sumP[phase] = 0;
+ samplesDuringThisMainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+ datalogCountInMainsCycles++;
+
+ // Changling the state of the loads is is a 3-part process:
+ // - change the LOGICAL load states as necessary to maintain the energy level
+ // - update the PHYSICAL load states according to the logical -> physical mapping
+ // - update the driver lines for each of the loads.
+ //
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_main > midPointOfEnergyBucket_main)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ upperEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_main)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_main;
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('+');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ lowerEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('-');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update the control ports for each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_main > capacityOfEnergyBucket_main) {
+ energyInBucket_main = capacityOfEnergyBucket_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod));
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod));
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+//
+ Serial.print(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+*/
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+
+
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ Serial.print(logicalLoadState[i]);
+ }
+ Serial.println();
+*/
+ }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for removing the DC
+ // component from the phase that is being processed.
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12);
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ if (phase == 0)
+ {
+ checkLoadPrioritySelection(); // updates load priorities if the switch is changed
+ }
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when the beyondStartUpPhase flag is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThisMainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+
+ // Applying max and min limits to the main accumulator's level
+ // is deferred until after the energy related decisions have been taken
+ //
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // NB. the index cannot be a 'byte' because the loop would not terminate correctly!
+ for (char index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_main = ");
+ Serial.println(capacityOfEnergyBucket_main);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino
new file mode 100644
index 0000000..acc1ca1
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino
@@ -0,0 +1,971 @@
+/* Mk2_3phase_RFdatalog_4.ino
+ *
+ * Issue 1 was released in January 2015.
+ *
+ * This sketch provides continuous monitoring of real power on three phases.
+ * Surplus power is diverted to multiple loads in sequential order. A suitable
+ * output-stage is required for each load; this can be either triac-based, or a
+ * Solid State Relay.
+ *
+ * Datalogging of real power and Vrms is provided for each phase.
+ * The presence or absence of the RFM12B needs to be set at compile time
+ *
+ * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes:
+ * - Improved control of multiple loads has been imported from the
+ * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino
+ * - the ISR has been upgraded to fix a possible timing anomaly
+ * - variables to store ADC samples are now declared as "volatile"
+ * - for RF69 RF module is now supported
+ * - a performance check has been added with the result being sent to the Serial port
+ * - control signals for loads are now active-high to suit the latest 3-phase PCB
+ *
+ * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - The reported power at each of the phases has been inverted. These values are now in
+ * line with the Open Energy Monitor convention, whereby import is positive and
+ * export is negative.
+ *
+ * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes:
+ * - removal of some redundant code in the logic for determining the next load state.
+ *
+ * July 2022: updated to Mk2_3phase_RFdatalog_4, with this change:
+ * - the datalogging accumulator for Vsquared has been rescaled to 1/16 of its previous value
+ * to avoid the risk of overflowing during a 20-second datalogging period.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include // may not be needed, but it's probably a good idea to include this
+
+#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+//#define RF69_COMPAT 0 // for the RFM12B
+#define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// In this sketch, the ADC is free-running with a cycle time of ~104uS.
+
+// WORKLOAD_CHECK is available for determining how much spare processing time there
+// is. To activate this mode, the #define line below should be included:
+//#define WORKLOAD_CHECK
+
+#define CYCLES_PER_SECOND 50
+//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging
+#define WORKING_ZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+#define NO_OF_PHASES 3
+#define DATALOG_PERIOD_IN_SECONDS 10
+
+const byte noOfDumploads = 3;
+
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL};
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB)
+enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB)
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// For this multi-load version, the same mechanism has been retained but the
+// output mode is hard-coded as below:
+enum outputModes outputMode = ANTI_FLICKER;
+
+// In this multi-load version, the external switch is re-used to determine the load priority
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY;
+
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10;
+const int networkGroup = 210;
+const int UNO = 1;
+#endif
+
+typedef struct {
+ int power_L1;
+ int power_L2;
+ int power_L3;
+ int Vrms_L1;
+ int Vrms_L2;
+ int Vrms_L3;} Tx_struct; // revised data for RF comms
+Tx_struct tx_data;
+
+
+// ----------- Pinout assignments -----------
+//
+// digital pins:
+const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB
+// D4 is not in use
+const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB)
+const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB)
+const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB)
+// D8 is not in use
+// D9 is not in use
+
+// analogue input pins
+const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB
+const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB
+
+
+// -------------- general global variables -----------------
+//
+// Some of these variables are used in multiple blocks so cannot be static.
+// For integer maths, some variables need to be 'long'
+//
+boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle
+byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor
+byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+
+long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF (min limit)
+long DCoffset_V_max; // <--- for LPF (max limit)
+int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale
+
+// for 3-phase use, with units of Joules * CYCLES_PER_SECOND
+float capacityOfEnergyBucket_main;
+float energyInBucket_main;
+float midPointOfEnergyBucket_main;
+float lowerThreshold_default;
+float lowerEnergyThreshold;
+float upperThreshold_default;
+float upperEnergyThreshold;
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4
+
+// for improved control of multiple loads
+boolean recentTransition = false;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only
+byte activeLoad = 0;
+
+// for datalogging
+int datalogCountInMainsCycles;
+const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND;
+float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging
+
+// for interaction between the main processor and the ISR
+volatile boolean dataReady = false;
+volatile int sampleV[NO_OF_PHASES];
+volatile int sampleI[NO_OF_PHASES];
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int mainsCycles_forContinuityChecker;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A compact explanation of each of these values now follows:
+
+// When calculating real power, which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043};
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// NB. Any tool which determines the optimal value of phaseCal must have a similar
+// scheme for taking sample values as does this sketch.
+//
+const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only
+int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths
+
+// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is
+// similar to the actual range of volts, the optimal value for this cal factor is likely to be
+// close to unity.
+const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter
+
+
+
+
+void setup()
+{
+ delay (initialDelay * 1000); // allows time to open the Serial Monitor
+
+ Serial.begin(9600); // initialize Serial interface
+ Serial.println();
+ Serial.println();
+ Serial.println();
+ Serial.println("----------------------------------");
+ Serial.println("Sketch ID: Mk2_3phase_RFdatalog_4.ino");
+
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2
+ pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ }
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state.
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic).
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic).
+
+ pinMode(loadPrioritySelectorPin, INPUT);
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode
+
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // When using integer maths, calibration values that have been supplied in
+ // floating point form need to be rescaled.
+ phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths
+ DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ }
+
+ // Define operating limits for the LP filters which identify DC offset in the voltage
+ // sample streams. By limiting the output range, these filters always should start up
+ // correctly.
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+ DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale
+
+ // for the main energy bucket
+ capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND;
+ midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds
+ energyInBucket_main = 0;
+
+ Serial.println ("ADC mode: free-running");
+ Serial.print ("requiredExport in Watts = ");
+ Serial.println (REQUIRED_EXPORT_IN_WATTS);
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples (3 pairs). The main processor and
+// the ADC work autonomously, their operation being synchnonised only via the dataReady flag.
+//
+void processRawSamples()
+{
+ static long sumP[NO_OF_PHASES];
+ static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection
+ static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm
+ static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage)
+ static int samplesDuringThisMainsCycle[NO_OF_PHASES];
+ static long sum_Vsquared[NO_OF_PHASES];
+ static long samplesDuringThisDatalogPeriod;
+ enum polarities polarityNow;
+
+ // The raw V and I samples are processed in "phase pairs"
+ for (byte phase = 0; phase < NO_OF_PHASES; phase++)
+ {
+ // remove DC offset from each raw voltage sample by subtracting the accurate value
+ // as determined by its associated LP filter.
+ long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase];
+
+ // determine polarity, to aid the logical flow
+ if(sampleV_minusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (polarityOfLastSampleV[phase] != POSITIVE)
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This is the start of a new +ve half cycle, for this phase, just after the
+ // zero-crossing point. Before the contribution from this phase can be added
+ // to the running total, the cal factor for this phase must be applied.
+ //
+ float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase];
+
+ processLatestContribution(phase, realPower); // runs at 6.6 ms intervals
+
+ // A performance check to monitor and display the minimum number of sets of
+ // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05
+ //
+ if (phase == 0)
+ {
+ if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle)
+ {
+ lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase];
+ }
+ mainsCycles_forContinuityChecker++;
+ if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ mainsCycles_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+ }
+
+ sumP[phase] = 0;
+ samplesDuringThisMainsCycle[phase] = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (initialDelay + startUpPeriod) * 1000)
+ {
+ beyondStartUpPeriod = true;
+ mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ Serial.println ("Go!");
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set
+ {
+ if (beyondStartUpPeriod)
+ {
+ // This code is executed once per 20mS, shortly after the start of each new
+ // mains cycle on phase 0.
+ //
+ datalogCountInMainsCycles++;
+
+ // Changling the state of the loads is is a 3-part process:
+ // - change the LOGICAL load states as necessary to maintain the energy level
+ // - update the PHYSICAL load states according to the logical -> physical mapping
+ // - update the driver lines for each of the loads.
+ //
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_main > midPointOfEnergyBucket_main)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ upperEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_main)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_main;
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('+');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_main < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ lowerEnergyThreshold = energyInBucket_main;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ Serial.print('-');
+ Serial.println(activeLoad);
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update the control ports for each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger
+ digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_main > capacityOfEnergyBucket_main) {
+ energyInBucket_main = capacityOfEnergyBucket_main; }
+ else
+ if (energyInBucket_main < 0) {
+ energyInBucket_main = 0; }
+
+ if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles)
+ {
+ datalogCountInMainsCycles = 0;
+
+ // To provide sufficient range for a dataloging period of at least 20 seconds, the accumulator
+ // for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value.
+ // Hence the * 4 factor that appears below after the sqrt() operation below.
+ //
+ tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles;
+ tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles;
+ tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles;
+ tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod) * 4);
+ tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod) * 4);
+ tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod) * 4);
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ Serial.print(energyInBucket_main / CYCLES_PER_SECOND);
+ Serial.print(", ");
+ Serial.println(tx_data.power_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L2);
+ Serial.print(", ");
+ Serial.print(tx_data.power_L3);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L1);
+ Serial.print(", ");
+ Serial.print(tx_data.Vrms_L2);
+ Serial.print(", ");
+ Serial.println(tx_data.Vrms_L3);
+*/
+ energyStateOfPhase[0] = 0;
+ energyStateOfPhase[1] = 0;
+ energyStateOfPhase[2] = 0;
+ sum_Vsquared[0] = 0;
+ sum_Vsquared[1] = 0;
+ sum_Vsquared[2] = 0;
+ samplesDuringThisDatalogPeriod = 0;
+
+
+/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements!
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ Serial.print(logicalLoadState[i]);
+ }
+ Serial.println();
+*/
+ }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polarity of this sample is negative
+ {
+ if (polarityOfLastSampleV[phase] != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // This is a convenient point to update the Low Pass Filter for removing the DC
+ // component from the phase that is being processed.
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12);
+ cumVdeltasThisCycle_long[phase] = 0;
+
+ // To ensure that this LP filter will always start up correctly when 240V AC is
+ // available, its output value needs to be prevented from drifting beyond the likely range
+ // of the voltage signal.
+ //
+ if (DCoffset_V_long[phase] < DCoffset_V_min) {
+ DCoffset_V_long[phase] = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long[phase] > DCoffset_V_max) {
+ DCoffset_V_long[phase] = DCoffset_V_max; }
+
+ if (phase == 0)
+ {
+ checkLoadPrioritySelection(); // updates load priorities if the switch is changed
+ }
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+
+ // Processing for EVERY pair of samples. Most of this code is not used during the
+ // start-up period, but it does no harm to leave it in place. Accumulated values
+ // are cleared when the beyondStartUpPhase flag is set to true.
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the current when a
+ // resistive load is used
+ long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase]
+ + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8);
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12)
+ instP = instP>>12; // reduce to 20-bits (x1)
+ sumP[phase] +=instP; // scaling is x1
+
+ // for the Vrms calculation (for datalogging only)
+ long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (x4096, or 2^12)
+ // inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-(
+ inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4), for more datalog range :-)
+ sum_Vsquared[phase] += inst_Vsquared; // scaling is x1/16
+ if (phase == 0) {
+ samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase
+
+ // general housekeeping
+ cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter
+ samplesDuringThisMainsCycle[phase] ++;
+
+ // store items for use during next loop
+ lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries
+ }
+}
+// end of processRawSamples()
+
+
+void processLatestContribution(byte phase, float power)
+{
+ float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND
+
+ // add the latest energy contribution to the relevant per-phase accumulator
+ // (only used for datalogging of power)
+ energyStateOfPhase[phase] += latestEnergyContribution;
+
+ // add the latest energy contribution to the main energy accumulator
+ energyInBucket_main += latestEnergyContribution;
+
+ // apply any adjustment that is required.
+ if (phase == 0)
+ {
+ energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50
+ }
+
+ // Applying max and min limits to the main accumulator's level
+ // is deferred until after the energy related decisions have been taken
+ //
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // NB. the index cannot be a 'byte' because the loop would not terminate correctly!
+ for (char index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCcount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCcount++;
+ }
+ if (loadPrioritySwitchCcount >= 20)
+ {
+ loadPrioritySwitchCcount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+
+
+// Although this sketch always operates in ANTI_FLICKER mode, it was convenient
+// to leave this mechanism in place.
+//
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_main * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_main = ");
+ Serial.println(capacityOfEnergyBucket_main);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+{
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
+
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino
new file mode 100644
index 0000000..cda0398
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino
@@ -0,0 +1,1052 @@
+/* Mk2_RF_datalog_1.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine dataloogig is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * April 2014
+ */
+
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+#include
+
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define RF_SEND_PERIOD 2 // seconds
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_433MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD
+const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove)
+ // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader
+
+int messageNumber = 0;
+
+// data structure for RF comms
+typedef struct {
+ int msgNumber;
+ int powerAtSupplyPoint;
+ int divertedEnergyTotal;
+} Tx_struct;
+Tx_struct tx_data; // an instance of this structure type
+
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte outputModeSelectorPin = 3; // <-- with the internal pullup
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long cycleCount = 0; // counts mains cycles from start-up
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_1.ino");
+ Serial.println();
+
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // control lines for the 74HC4543 7-seg display driver and the DP line
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+ // control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+
+ Serial.println ("----");
+
+#ifdef WORKLOAD_CHECK
+ Serial.println ("WELCOME TO WORKLOAD_CHECK ");
+
+// <<- start of commented out section, to save on RAM space!
+/*
+ Serial.println (" This mode of operation allows the spare processing capacity of the system");
+ Serial.println ("to be analysed. Additional delay is gradually increased until all spare time");
+ Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. ");
+ Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is ");
+ Serial.println ("checked several times before the delay is increased. ");
+ */
+// <<- end of commented out section, to save on RAM space!
+
+ Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, ");
+ Serial.println ("that is available for doing additional processing.");
+ Serial.println ();
+ #endif
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+ rf12_sleep(RF12_SLEEP);
+
+}
+
+// An Interrupt Service Routine is now defined in which the ADC is instructed to
+// measure V and I alternately. A "data ready"flag is set after each voltage conversion
+// has been completed.
+// For each pair of samples, this means that current is measured before voltage. The
+// current sample is taken first because the phase of the waveform for current is generally
+// slightly advanced relative to the waveform for voltage. The data ready flag is cleared
+// within loop().
+// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is
+// executed whenever the ADC timer expires. In this mode, the next ADC conversion is
+// initiated from within this ISR.
+//
+void timerIsr(void)
+{
+ static unsigned char sample_index = 0;
+
+ switch(sample_index)
+ {
+ case 0:
+ sampleV = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current
+ ADCSRA |= (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static int samplesDuringThisCycle; // for normalising the power in each mains cycle
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static enum polarities polarityOfLastSampleV; // for zero-crossing detection
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte perSecondCounter = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // extra items for datalogging
+ static long sumP_atSupplyPoint;
+ static unsigned int samplesDuringDatalogPeriod;
+ static int RF_send_counter = 0; // counts seconds
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ enum polarities polarityNow;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+ cycleCount++;
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) {
+ realEnergy_diverted = 0; } // to avoid 'creep'
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ perSecondCounter++;
+ if(perSecondCounter >= CYCLES_PER_SECOND)
+ {
+ perSecondCounter = 0;
+/*
+ Serial.print("Diverted: " );
+ Serial.print(divertedEnergyTotal_Wh);
+ Serial.print(" Wh plus ");
+ Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU);
+
+ Serial.print(" J , EDD is" );
+*/
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+/*
+ if (EDD_isActive) {
+ Serial.println(" on" ); }
+ else {
+ Serial.println(" off" ); }
+*/
+ configureValueForDisplay(); // occurs every second
+
+ // routine data is to be transmitted every N seconds
+ RF_send_counter++;
+ if (RF_send_counter >= RF_SEND_PERIOD)
+ {
+ RF_send_counter = 0;
+
+ // calculate the average power at the supply point
+ long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod;
+ tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid;
+ sumP_atSupplyPoint = 0;
+ samplesDuringDatalogPeriod = 0;
+
+ tx_data.msgNumber = messageNumber;
+ tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh;
+ send_rf_data();
+ Serial.print(tx_data.msgNumber);
+ Serial.print(", ");
+ Serial.print(tx_data.powerAtSupplyPoint);
+ Serial.print(", ");
+ Serial.println(tx_data.divertedEnergyTotal);
+ messageNumber++;
+ }
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ samplesDuringThisCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (samplesDuringThisCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ samplesDuringThisCycle = 0;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, for datalogging
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ samplesDuringThisCycle++;
+ samplesDuringDatalogPeriod++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void send_rf_data()
+{
+ rf12_sleep(RF12_WAKEUP);
+ // if ready to send + exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendStart(0, &tx_data, sizeof tx_data);
+ rf12_sendWait(2);
+ rf12_sleep(RF12_SLEEP);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino
new file mode 100644
index 0000000..a7da0ff
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino
@@ -0,0 +1,1068 @@
+/* Mk2_RF_datalog_2.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine dataloogig is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * April 2014
+ */
+
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+#include
+
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define RF_SEND_PERIOD 2 // seconds
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_868MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD
+const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove)
+ // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader
+
+int messageNumber = 0;
+
+// data structure for RF comms
+typedef struct {
+// int msgNumber;
+ int powerAtSupplyPoint; // import = +ve, to match OEM convention
+ int divertedEnergyTotal; // always positive
+} Tx_struct;
+Tx_struct tx_data; // an instance of this structure type
+
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte outputModeSelectorPin = 3; // <-- with the internal pullup
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long cycleCount = 0; // counts mains cycles from start-up
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy diversion detection
+long requiredExportPerMainsCycle_inIEU;
+float IEUtoJoulesConversion_CT1;
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_2.ino");
+ Serial.println();
+
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // control lines for the 74HC4543 7-seg display driver and the DP line
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+ // control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND;
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+
+ Serial.println ("----");
+
+#ifdef WORKLOAD_CHECK
+ Serial.println ("WELCOME TO WORKLOAD_CHECK ");
+
+// <<- start of commented out section, to save on RAM space!
+/*
+ Serial.println (" This mode of operation allows the spare processing capacity of the system");
+ Serial.println ("to be analysed. Additional delay is gradually increased until all spare time");
+ Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. ");
+ Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is ");
+ Serial.println ("checked several times before the delay is increased. ");
+ */
+// <<- end of commented out section, to save on RAM space!
+
+ Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, ");
+ Serial.println ("that is available for doing additional processing.");
+ Serial.println ();
+ #endif
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+}
+
+// An Interrupt Service Routine is now defined in which the ADC is instructed to
+// measure V and I alternately. A "data ready"flag is set after each voltage conversion
+// has been completed.
+// For each pair of samples, this means that current is measured before voltage. The
+// current sample is taken first because the phase of the waveform for current is generally
+// slightly advanced relative to the waveform for voltage. The data ready flag is cleared
+// within loop().
+// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is
+// executed whenever the ADC timer expires. In this mode, the next ADC conversion is
+// initiated from within this ISR.
+//
+void timerIsr(void)
+{
+ static unsigned char sample_index = 0;
+
+ switch(sample_index)
+ {
+ case 0:
+ sampleV = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current
+ ADCSRA |= (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static int samplesDuringThisCycle; // for normalising the power in each mains cycle
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static enum polarities polarityOfLastSampleV; // for zero-crossing detection
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte perSecondCounter = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // extra items for datalogging
+ static long sumP_atSupplyPoint;
+ static unsigned int samplesDuringDatalogPeriod;
+ static int RF_send_counter = 0; // counts seconds
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ enum polarities polarityNow;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+ cycleCount++;
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts
+
+ realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) {
+ realEnergy_diverted = 0; } // to avoid 'creep'
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ perSecondCounter++;
+ if(perSecondCounter >= CYCLES_PER_SECOND)
+ {
+ perSecondCounter = 0;
+/*
+ Serial.print("Diverted: " );
+ Serial.print(divertedEnergyTotal_Wh);
+ Serial.print(" Wh plus ");
+ Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU);
+
+ Serial.print(" J , EDD is" );
+*/
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+/*
+ if (EDD_isActive) {
+ Serial.println(" on" ); }
+ else {
+ Serial.println(" off" ); }
+*/
+ configureValueForDisplay(); // occurs every second
+
+ // routine data is to be transmitted every N seconds
+ RF_send_counter++;
+ if (RF_send_counter >= RF_SEND_PERIOD)
+ {
+ RF_send_counter = 0;
+
+ // calculate the average power at the supply point
+ long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod;
+ tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid;
+ tx_data.powerAtSupplyPoint *= -1; // To match the OEM convention (so import is +ve)
+
+ sumP_atSupplyPoint = 0;
+ samplesDuringDatalogPeriod = 0;
+
+// tx_data.msgNumber = messageNumber;
+ tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh;
+ send_rf_data();
+// Serial.print(tx_data.msgNumber);
+// Serial.print(", ");
+ Serial.print(tx_data.powerAtSupplyPoint);
+ Serial.print(", ");
+ Serial.println(tx_data.divertedEnergyTotal);
+// messageNumber++;
+ }
+
+ Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1);
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ samplesDuringThisCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (samplesDuringThisCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ samplesDuringThisCycle = 0;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, for datalogging
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ samplesDuringThisCycle++;
+ samplesDuringDatalogPeriod++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void send_rf_data()
+{
+ // rf12_sleep(RF12_WAKEUP);
+ // if ready to send + exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+
+ // rf12_sendStart(0, &tx_data, sizeof tx_data);
+ // rf12_sendWait(2);
+ // rf12_sleep(RF12_SLEEP);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino
new file mode 100644
index 0000000..666d7cc
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino
@@ -0,0 +1,1074 @@
+/* Mk2_RF_datalog_3.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine dataloogig is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * September 2014
+ */
+
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+#include
+
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define RF_SEND_PERIOD 2 // seconds
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_868MHZ // Use the freq to match the module you have.
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD
+const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove)
+ // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader
+
+int messageNumber = 0;
+
+// data structure for RF comms
+typedef struct {
+// int msgNumber;
+ int powerAtSupplyPoint; // import = +ve, to match OEM convention
+ int divertedEnergyTotal; // always positive
+} Tx_struct;
+Tx_struct tx_data; // an instance of this structure type
+
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte outputModeSelectorPin = 3; // <-- with the internal pullup
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy diversion detection
+long requiredExportPerMainsCycle_inIEU;
+float IEUtoJoulesConversion_CT1;
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_3.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND;
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+
+ Serial.println ("----");
+
+#ifdef WORKLOAD_CHECK
+ Serial.println ("WELCOME TO WORKLOAD_CHECK ");
+
+// <<- start of commented out section, to save on RAM space!
+/*
+ Serial.println (" This mode of operation allows the spare processing capacity of the system");
+ Serial.println ("to be analysed. Additional delay is gradually increased until all spare time");
+ Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. ");
+ Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is ");
+ Serial.println ("checked several times before the delay is increased. ");
+ */
+// <<- end of commented out section, to save on RAM space!
+
+ Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, ");
+ Serial.println ("that is available for doing additional processing.");
+ Serial.println ();
+ #endif
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+}
+
+// An Interrupt Service Routine is now defined in which the ADC is instructed to
+// measure V and I alternately. A "data ready"flag is set after each voltage conversion
+// has been completed.
+// For each pair of samples, this means that current is measured before voltage. The
+// current sample is taken first because the phase of the waveform for current is generally
+// slightly advanced relative to the waveform for voltage. The data ready flag is cleared
+// within loop().
+// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is
+// executed whenever the ADC timer expires. In this mode, the next ADC conversion is
+// initiated from within this ISR.
+//
+void timerIsr(void)
+{
+ static unsigned char sample_index = 0;
+
+ switch(sample_index)
+ {
+ case 0:
+ sampleV = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current
+ ADCSRA |= (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static int samplesDuringThisCycle; // for normalising the power in each mains cycle
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static enum polarities polarityOfLastSampleV; // for zero-crossing detection
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte perSecondCounter = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // extra items for datalogging
+ static long sumP_atSupplyPoint;
+ static unsigned int samplesDuringDatalogPeriod;
+ static int RF_send_counter = 0; // counts seconds
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ enum polarities polarityNow;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts
+
+ realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) {
+ realEnergy_diverted = 0; } // to avoid 'creep'
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ perSecondCounter++;
+ if(perSecondCounter >= CYCLES_PER_SECOND)
+ {
+ perSecondCounter = 0;
+/*
+ Serial.print("Diverted: " );
+ Serial.print(divertedEnergyTotal_Wh);
+ Serial.print(" Wh plus ");
+ Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU);
+
+ Serial.print(" J , EDD is" );
+*/
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+/*
+ if (EDD_isActive) {
+ Serial.println(" on" ); }
+ else {
+ Serial.println(" off" ); }
+*/
+ configureValueForDisplay(); // occurs every second
+
+ // routine data is to be transmitted every N seconds
+ RF_send_counter++;
+ if (RF_send_counter >= RF_SEND_PERIOD)
+ {
+ RF_send_counter = 0;
+
+ // calculate the average power at the supply point
+ long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod;
+ tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid;
+ tx_data.powerAtSupplyPoint *= -1; // To match the OEM convention (so import is +ve)
+
+ sumP_atSupplyPoint = 0;
+ samplesDuringDatalogPeriod = 0;
+
+// tx_data.msgNumber = messageNumber;
+ tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh;
+ send_rf_data();
+// Serial.print(tx_data.msgNumber);
+// Serial.print(", ");
+ Serial.print(tx_data.powerAtSupplyPoint);
+ Serial.print(", ");
+ Serial.println(tx_data.divertedEnergyTotal);
+// messageNumber++;
+ }
+
+ Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1);
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ samplesDuringThisCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (samplesDuringThisCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ samplesDuringThisCycle = 0;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, for datalogging
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ samplesDuringThisCycle++;
+ samplesDuringDatalogPeriod++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void send_rf_data()
+{
+ // rf12_sleep(RF12_WAKEUP);
+ // if ready to send + exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+
+ // rf12_sendStart(0, &tx_data, sizeof tx_data);
+ // rf12_sendWait(2);
+ // rf12_sleep(RF12_SLEEP);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino
new file mode 100644
index 0000000..7463248
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino
@@ -0,0 +1,1102 @@
+/* Mk2_RFdatalog_4.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine dataloogig is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * December 2014
+ */
+
+#include
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_868MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+
+long copyOf_sumP_atSupplyPoint;
+long copyOf_sum_Vsquared;
+long copyOf_divertedEnergyTotal_Wh;
+int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define POLARITY_CHECK_MAXCOUNT 2
+enum polarities polarityNow;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_4.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+ send_rf_data();
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+
+ } // end of processing that is specific to the first +ve Vsample in each mains cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ } // leave the triac's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // Half way through the mains cycle, just after the -ve going zero-crossing point.
+ // This is a convenient place to update the Low Pass Filter for DC-offset removal.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered. It forms part of the ISR.
+ */
+ static byte count = 0;
+ if (polarityNow != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count >= POLARITY_CHECK_MAXCOUNT)
+ {
+ count = 0;
+ polarityConfirmed = polarityNow;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino
new file mode 100644
index 0000000..6e01d6d
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino
@@ -0,0 +1,1114 @@
+/* Mk2_RFdatalog_4a.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine datalogging is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#define RF69_COMPAT 0 // <-- include this line for the RFM12B
+// #define RF69_COMPAT 1 // <-- include this line for the RF69
+
+
+#include
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_433MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+
+long copyOf_sumP_atSupplyPoint;
+long copyOf_sum_Vsquared;
+long copyOf_divertedEnergyTotal_Wh;
+int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define POLARITY_CHECK_MAXCOUNT 2
+enum polarities polarityNow;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_4a.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+ send_rf_data();
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+
+ } // end of processing that is specific to the first +ve Vsample in each mains cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ } // leave the triac's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // Half way through the mains cycle, just after the -ve going zero-crossing point.
+ // This is a convenient place to update the Low Pass Filter for DC-offset removal.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered. It forms part of the ISR.
+ */
+ static byte count = 0;
+ if (polarityNow != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count >= POLARITY_CHECK_MAXCOUNT)
+ {
+ count = 0;
+ polarityConfirmed = polarityNow;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino
new file mode 100644
index 0000000..845bd63
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino
@@ -0,0 +1,1118 @@
+/* Mk2_RFdatalog_4b.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * Routine datalogging is also supported using the on-board RFM12B module.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#define RF69_COMPAT 0 // <-- include this line for the RFM12B
+// #define RF69_COMPAT 1 // <-- include this line for the RF69
+
+
+#include
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_433MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define POLARITY_CHECK_MAXCOUNT 2
+enum polarities polarityNow;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_4b.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+ send_rf_data();
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+
+ } // end of processing that is specific to the first +ve Vsample in each mains cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ } // leave the triac's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // Half way through the mains cycle, just after the -ve going zero-crossing point.
+ // This is a convenient place to update the Low Pass Filter for DC-offset removal.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered. It forms part of the ISR.
+ */
+ static byte count = 0;
+ if (polarityNow != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count >= POLARITY_CHECK_MAXCOUNT)
+ {
+ count = 0;
+ polarityConfirmed = polarityNow;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino
new file mode 100644
index 0000000..34ba24f
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino
@@ -0,0 +1,1134 @@
+/* Mk2_RFdatalog_5.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac
+ * or Solid State Relay. Routine datalogging is also supported using the
+ * on-board RF module (either RFM12B or RF69).
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+// #define RF69_COMPAT 0 // <-- include this line for the RFM12B
+#define RF69_COMPAT 1 // <-- include this line for the RF69
+
+
+#include
+#include
+#include // JeeLib is available at from: http://github.com/jcw/jeelib
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#define freq RF12_433MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_5.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+ send_rf_data();
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (beyondStartUpPhase)
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_ON; }
+ else {
+ } // leave the load's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfLoad);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfLoad == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino
new file mode 100644
index 0000000..e4c6513
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino
@@ -0,0 +1,1156 @@
+/* Mk2_RFdatalog_5a.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac
+ * or Solid State Relay. Routine datalogging is also supported using the
+ * on-board RF module (either RFM12B or RF69).
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include
+#include
+
+#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#ifdef RF_PRESENT
+#define freq RF12_868MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+#endif
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ Serial.print ("RF capability ");
+
+#ifdef RF_PRESENT
+ Serial.print ("IS present, freq = ");
+ if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); }
+ if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); }
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+#else
+ Serial.println ("is NOT present");
+#endif
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (beyondStartUpPhase)
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_ON; }
+ else {
+ } // leave the load's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfLoad);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfLoad == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino
new file mode 100644
index 0000000..ad9598b
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino
@@ -0,0 +1,1173 @@
+/* Mk2_RFdatalog_6.ino
+ *
+ * This sketch is for diverting suplus PV power to a dump load using a triac
+ * or Solid State Relay. Routine datalogging is also supported using the
+ * on-board RF module (either RFM12B or RF69).
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * November 2020: updated to Mk2_RFdatalog_6, with these changes:
+ * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This
+ * allows the datalogging period to be extended beyond 5 seconds without the counter
+ * running out of range.
+ * - diverted energy, as monitored by CT2, is now reported as an average power as well as
+ * the cumulative energy total for the current day.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include
+#include
+
+#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 500
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+#endif
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int divertedPower_Watts; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_forDivertedEnergy; // for per-cycle summation of diverted energy
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sumP_forDivertedPower; // for summation of diverted power values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+int cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sumP_forDivertedPower;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.062;
+const float powerCal_diverted = 0.062;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_6.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ Serial.print ("RF capability ");
+
+#ifdef RF_PRESENT
+ Serial.print ("IS present, freq = ");
+ if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); }
+ if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); }
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+#else
+ Serial.println ("is NOT present");
+#endif
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.divertedPower_Watts = copyOf_sumP_forDivertedPower * powerCal_diverted / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+ tx_data.temperature_times100 = readTemperature();
+
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+
+ Serial.print("grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", diverted power "); Serial.print(tx_data.divertedPower_Watts);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(" [minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+// Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(']');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ //
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ //
+ sumP_forDivertedEnergy +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_forDivertedPower +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint = 0;
+ sumP_forDivertedEnergy = 0;
+ sumP_forDivertedPower = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (beyondStartUpPhase)
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_ON; }
+ else {
+ } // leave the load's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfLoad);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfLoad == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_forDivertedEnergy / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_forDivertedEnergy = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_sumP_forDivertedPower = sumP_forDivertedPower;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sumP_forDivertedPower = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino
new file mode 100644
index 0000000..f642a57
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino
@@ -0,0 +1,1190 @@
+/* Mk2_RFdatalog_7.ino
+ *
+ * This sketch is for diverting suplus PV power to a dump load using a triac
+ * or Solid State Relay. Routine datalogging is also supported using the
+ * on-board RF module (either RFM12B or RF69).
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RFM12B module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * November 2020: updated to Mk2_RFdatalog_6, with these changes:
+ * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This
+ * allows the datalogging period to be extended beyond 5 seconds without the counter
+ * running out of range.
+ * - diverted energy, as monitored by CT2, is now reported as an average power as well as
+ * the cumulative energy total for the current day.
+ *
+ * July 2022: updated to Mk2_RFdatalog_7, with this change:
+ * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled
+ * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second
+ * datalogging period.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include
+#include
+
+#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 500
+// #define POST_DATALOG_EVENT_DELAY_MILLIS 40
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+
+// ---- Output mode selection -----
+enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+//enum outputModes outputMode = NORMAL; // external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#ifdef RF_PRESENT
+#define freq RF12_433MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+#endif
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int divertedPower_Watts; // always positive
+ int Vrms_times100;
+ int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+const byte tempSensorPin = 3; // <-- the "mode" port
+const byte outputForTrigger = 4;
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning load off
+long upperEnergyThreshold_long; // for turning load on
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_forDivertedEnergy; // for per-cycle summation of diverted energy
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sumP_forDivertedPower; // for summation of diverted power values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+int cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sumP_forDivertedPower;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+// For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.062;
+const float powerCal_diverted = 0.062;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // energy diversion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_7.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+ convertTemperature(); // start initial temperature conversion
+
+ Serial.print ("RF capability ");
+
+#ifdef RF_PRESENT
+ Serial.print ("IS present, freq = ");
+ if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); }
+ if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); }
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+#else
+ Serial.println ("is NOT present");
+#endif
+
+ convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ // To provide sufficient range for a dataloging period of 10 seconds, the accumulators for grid power
+ // and diverted power are now scaled at 1/16 of their previous V_ADC * I_ADC values. Hence the * 16 factor
+ // that appears below.
+ // Similarly, the accumulator for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value.
+ // Hence the * 4 factor that appears below after the sqrt() operation.
+ //
+ tx_data.powerAtSupplyPoint_Watts =
+ copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod * 16;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.divertedPower_Watts =
+ copyOf_sumP_forDivertedPower * powerCal_diverted / copyOf_sampleSetsDuringThisDatalogPeriod * 16;
+ tx_data.Vrms_times100 =
+ (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod) * 4);
+ tx_data.temperature_times100 = readTemperature();
+
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+
+ Serial.print("grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", diverted power "); Serial.print(tx_data.divertedPower_Watts);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+ Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(" [minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+// Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(']');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+ convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (x4096, or 2^12)
+// inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-(
+ inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4) for more datalog range
+ sum_Vsquared += inst_Vsquared; // scaling is x1/16
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12)
+ instP = instP>>12; // reduce to 20-bits (x1)
+ sumP_forEnergyBucket+=instP; // scaling is x1
+ //
+ instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range
+ sumP_atSupplyPoint +=instP; // scaling is x1/16
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12)
+ instP = instP>>12; // reduce to 20-bits (x1)
+ sumP_forDivertedEnergy +=instP; // scaling is x1
+ //
+ instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range
+ sumP_forDivertedPower +=instP; // scaling is x1/16
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+ static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint = 0;
+ sumP_forDivertedEnergy = 0;
+ sumP_forDivertedPower = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+ if (beyondStartUpPhase)
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the load to "off"
+ nextStateOfLoad = LOAD_ON; }
+ else {
+ } // leave the load's state unchanged (hysteresis)
+
+ // set the Arduino's output pin accordingly
+ digitalWrite(outputForTrigger, nextStateOfLoad);
+
+ // update the Energy Diversion Detector
+ if (nextStateOfLoad == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_forDivertedEnergy / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_forDivertedEnergy = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_sumP_forDivertedPower = sumP_forDivertedPower;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sumP_forDivertedPower = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+// this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+
+
+#ifdef RF_PRESENT
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino
new file mode 100644
index 0000000..d926631
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino
@@ -0,0 +1,1400 @@
+/* Mk2_RFdatalog_multiLoad_1.ino is based on Mk2_RFdatalog_5a
+ *
+ * This sketch is for diverting surplus PV power to one or two dump loads using
+ * triac-based output stages or Solid State Relays. Routine datalogging is
+ * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display
+ * showing the Diverted Energy each day is also supported.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RF module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes:
+ * - temperature sensing commented out(formally supported via D3 at the "mode" port")
+ * - support for a second load added (vcontrolled via D3 at the "mode" port")
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include
+// #include // for temperature sensing
+
+#define RF_PRESENT // <- this line must be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+/*
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+*/
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+const byte noOfDumploads = 2;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// ---- Output mode selection -----
+// enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+enum outputModes outputMode = NORMAL; // external switch is in use
+
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; // <- needs to be set here unless an
+// enum loadPriorityModes loadPriorityMode = LOAD_1_HAS_PRIORITY; // <- external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#ifdef RF_PRESENT
+#define freq RF12_868MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+#endif
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+// int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+//const byte tempSensorPin = 3; // <-- the "mode" port
+const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active high
+const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active low
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 2; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+long lowerThreshold_default;
+long lowerEnergyThreshold;
+long upperThreshold_default;
+long upperEnergyThreshold;
+
+boolean recentTransition;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+byte activeLoad = 0;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+/*
+ * // For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+*/
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // (for test purposes only)
+
+
+void setup()
+{
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ physicalLoadState[i] = LOAD_OFF;
+ }
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low.
+ digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high.
+
+/*
+ pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority
+*/
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2;
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+// convertTemperature(); // start initial temperature conversion
+
+ Serial.print ("RF capability ");
+
+#ifdef RF_PRESENT
+ Serial.print ("IS present, freq = ");
+ if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); }
+ if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); }
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+#else
+ Serial.println ("is NOT present");
+#endif
+
+// convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+// tx_data.temperature_times100 = readTemperature();
+
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+// Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+// convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+// static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+
+ if (beyondStartUpPhase)
+ {
+ /* Determining whether any of the loads need to be changed is is a 3-stage process:
+ * - change the LOGICAL load states as necessary to maintain the energy level
+ * - update the PHYSICAL load states according to the logical -> physical mapping
+ * - update the driver lines for each of the loads.
+ */
+
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_long > midPointOfEnergyBucket_long)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_long > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ if (energyInBucket_long > upperEnergyThreshold)
+ {
+ upperEnergyThreshold = energyInBucket_long;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_long)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_long;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_long < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ if (energyInBucket_long < lowerEnergyThreshold)
+ {
+ lowerEnergyThreshold = energyInBucket_long;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load
+
+ // update the Energy Diversion Detector
+ if (physicalLoadState[0] == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // this index counter can't be a 'byte' because the loop would run forever!
+ //
+ for (int index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+
+
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+/*
+this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+*/
+
+/*
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCount++;
+ }
+ if (loadPrioritySwitchCount >= 20)
+ {
+ loadPrioritySwitchCount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+*/
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_long * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+/*
+ * void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+*/
+
+#ifdef RF_PRESENT
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino
new file mode 100644
index 0000000..70c437d
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino
@@ -0,0 +1,1400 @@
+/* Mk2_RFdatalog_multiLoad_1a.ino
+ *
+ * This sketch is for diverting suplus PV power to one or two dump loads using
+ * triac-based output stages or Solid State Relays. Routine datalogging is
+ * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display
+ * showing the Diverted Energy each day is also supported.
+ *
+ * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router.
+ * The integral voltage sensor is fed from one of the secondary coils of the
+ * transformer. Current is measured via Current Transformers at the
+ * CT1 and CT1 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. When the RF module is
+ * in use, the display can only be used in conjunction with an extra pair of
+ * logic chips. These are ICs 3 and 4, which reduce the number of processor pins
+ * that are needed to drive the display.
+ *
+ * This sketch is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * September 2014: renamed as Mk2_RFdatalog_3, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - tidier initialisation of display logic in setup();
+ *
+ * December 2014, upgraded to Mk2_RFdatalog_4:
+ * This sketch has been restructured in order to make better use of the ISR. All of
+ * the time-critical code is now contained within the ISR and its helper functions.
+ * Values for datalogging are transferred to the main code using a flag-based handshake
+ * mechanism. The diversion of surplus power can no longer be affected by slower
+ * activities which may be running in the main code such as Serial statements and RF.
+ * Temperature sensing is supported by re-allocating the "mode" port for this
+ * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The
+ * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time.
+ * Also:
+ * - The ADC is now in free-running mode, at ~104 us per conversion.
+ * - a persistence check has been added for zero-crossing detection (polarityConfirmed)
+ * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances
+ * - Vrms has been added to the datalog payload (as Vrms x 100)
+ * - temperature has been added to the datalog payload (as degrees C x 100)
+ * - the phaseCal mechanism has been reinstated
+ *
+ * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate
+ * the phaseCal calculation. Previously, this feature was having no effect because
+ * two assignment lines were in the wrong order. When measuring "real power", which
+ * is what this application does, the phaseCal refinement has very little effect even
+ * when correctly implemented, as it now is.
+ * Support for the RF69 RF module has also been added.
+ *
+ * January 2016: updated to Mk2_RFdatalog_4b:
+ * The variables to store copies of ADC results for use by the main code are now declared
+ * as "volatile" to remove any possibility of incorrect operation due to optimisation
+ * by the compiler.
+ *
+ * February 2016: updated to Mk2_RFdatalog_5, with these changes:
+ * - improvements to the start-up logic. The start of normal operation is now
+ * synchronised with the start of a new mains cycle.
+ * - reduce the amount of feedback in the Low Pass Filter for removing the DC content
+ * from the Vsample stream. This resolves an anomaly which has been present since
+ * the start of this project. Although the amount of feedback has previously been
+ * excessive, this anomaly has had minimal effect on the system's overall behaviour.
+ * - tidying of the "confirmPolarity" logic to make its behaviour more clear
+ * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES
+ * - change "triac" to "load" wherever appropriate
+ *
+ * March 2016: updated to Mk2_RFdatalog_5a, with this change:
+ * - RF capability made switchable so that the code will continue to run
+ * when an RF module is not fitted. Dataloging can then take place
+ * via the Serial port.
+ *
+ * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes:
+ * - temperature sensing commented out(formally supported via D3 at the "mode" port")
+ * - support for a second load added (vcontrolled via D3 at the "mode" port")
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ */
+
+#include
+// #include // for temperature sensing
+
+#define RF_PRESENT // <- this line must be commented out if the RFM12B module is not present
+
+#ifdef RF_PRESENT
+#define RF69_COMPAT 0 // for the RFM12B
+// #define RF69_COMPAT 1 // for the RF69
+#include
+#endif
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// -----------------------------------------------------
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define WORKING_RANGE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+/*
+// --------------------------
+// Dallas DS18B20 commands
+#define SKIP_ROM 0xcc
+#define CONVERT_TEMPERATURE 0x44
+#define READ_SCRATCHPAD 0xbe
+#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present
+*/
+
+// ----------------
+// general literals
+#define DATALOG_PERIOD_IN_MAINS_CYCLES 250
+#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping'
+ // in Joules per mains cycle (has no effect when set to 0)
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+const byte noOfDumploads = 2;
+
+// -------------------------------
+// definitions of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions.
+enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY};
+
+enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low
+enum loadStates logicalLoadState[noOfDumploads];
+enum loadStates physicalLoadState[noOfDumploads];
+
+// ---- Output mode selection -----
+// enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an
+enum outputModes outputMode = NORMAL; // external switch is in use
+
+enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; // <- needs to be set here unless an
+// enum loadPriorityModes loadPriorityMode = LOAD_1_HAS_PRIORITY; // <- external switch is in use
+
+/* --------------------------------------
+ * RF configuration (for the RFM12B module)
+ * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ
+ */
+#ifdef RF_PRESENT
+#define freq RF12_868MHZ
+
+const int nodeID = 10; // RFM12B node ID
+const int networkGroup = 210; // wireless network group - needs to be same for all nodes
+const int UNO = 1; // for when the processor contains the UNO bootloader.
+#endif
+
+typedef struct {
+ int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention
+ int divertedEnergyTotal_Wh; // always positive
+ int Vrms_times100;
+// int temperature_times100;
+} Tx_struct;
+Tx_struct tx_data;
+
+// allocation of digital pins when pin-saving hardware is in use
+// *************************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is for the RFM12B
+//const byte tempSensorPin = 3; // <-- the "mode" port
+const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active high
+const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active low
+// D5 is the enable line for the 7-segment display driver, IC3
+// D6 is a data input line for the 7-segment display driver, IC3
+// D7 is a data input line for the 7-segment display driver, IC3
+// D8 is a data input line for the 7-segment display driver, IC3
+// D9 is a data input line for the 7-segment display driver, IC3
+// D10 is for the RFM12B
+// D11 is for the RFM12B
+// D12 is for the RFM12B
+// D13 is for the RFM12B
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is the decimal point driver line for the 4-digit display
+// A1 (D15) is a digit selection line for the 4-digit display, via IC4
+// A2 (D16) is a digit selection line for the 4-digit display, via IC4
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte delayBeforeSerialStarts = 2; // in seconds, to allow Serial window to be opened
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+/* -------------------------------------------------------------------------------------
+ * Global variables that are used in multiple blocks so cannot be static.
+ * For integer maths, many variables need to be 'long'
+ */
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load)
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic
+int phaseCal_grid_int; // to avoid the need for floating-point maths
+int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+float IEUtoJoulesConversion_CT1;
+
+long lowerThreshold_default;
+long lowerEnergyThreshold;
+long upperThreshold_default;
+long upperEnergyThreshold;
+
+boolean recentTransition;
+byte postTransitionCount;
+#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect
+byte activeLoad = 0;
+
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4
+
+long sumP_forEnergyBucket; // for per-cycle summation of 'real power'
+long sumP_diverted; // for per-cycle summation of diverted power
+long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period
+long sum_Vsquared; // for summation of V^2 values during datalog period
+int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle
+long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period
+
+long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+long lastSampleVminusDC_long; // for the phaseCal algorithm
+byte cycleCountForDatalogging = 0;
+long sampleVminusDC_long;
+long requiredExportPerMainsCycle_inIEU;
+
+// for interaction between the main code and the ISR
+volatile boolean datalogEventPending = false;
+volatile boolean newMainsCycle = false;
+volatile long copyOf_sumP_atSupplyPoint;
+volatile long copyOf_sum_Vsquared;
+volatile long copyOf_divertedEnergyTotal_Wh;
+volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle;
+volatile long copyOf_sampleSetsDuringThisDatalogPeriod;
+
+/*
+ * // For temperature sensing
+OneWire oneWire(tempSensorPin);
+int tempTimes100;
+*/
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define PERSISTENCE_FOR_POLARITY_CHANGE 2
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed; // for zero-crossing detection
+enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection
+
+// For a mechanism to check the integrity of this code structure
+int lowestNoOfSampleSetsPerMainsCycle;
+unsigned long timeAtLastDelay;
+
+
+// Calibration values (not important for the Router's basic operation)
+//-------------------
+// For accurate calculation of real power/energy, two calibration values are
+// used: powerCal and phaseCal. With most hardware, the default values are
+// likely to work fine without need for change. A full explanation of each
+// of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured. Any pre-built system that I supply will have been
+// checked with this tool to ensure that the input sensors are working correctly.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 230V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+// for 3.3V operation, the optimum value is generally around 0.044
+// for 5V operation, the optimum value is generally around 0.072
+//
+const float powerCal_grid = 0.072;
+const float powerCal_diverted = 0.073;
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. This mechanism can be used to offset any difference in
+// phase delay between the voltage and current sensors. The algorithm interpolates
+// between the most recent pair of voltage samples according to the phaseCal value.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value is used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1.
+//
+// The calculation for real power is very insensitive to the value of phaseCal.
+// When a "real power" calculation is used to determine how much surplus energy
+// is available for diversion, a nominal value such as 1.0 is generally thought
+// to be sufficient for this purpose.
+//
+const float phaseCal_grid = 1.0;
+const float phaseCal_diverted = 1.0;
+
+
+// For datalogging purposes, voltageCal has been included too. When running at
+// 230 V AC, the range of ADC values will be similar to the actual range of volts,
+// so the optimal value for this cal factor will be close to unity.
+//
+const float voltageCal = 1.0;
+
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of this array is for the decimal point status.
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+volatile boolean EDD_isActive = false; // energy diversion detection
+//volatile boolean EDD_isActive = true; // (for test purposes only)
+
+
+void setup()
+{
+ pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load
+ pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load
+
+ for(int i = 0; i< noOfDumploads; i++)
+ {
+ logicalLoadState[i] = LOAD_OFF;
+ physicalLoadState[i] = LOAD_OFF;
+ }
+
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low.
+ digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high.
+
+/*
+ pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector
+ digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and
+ loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority
+*/
+
+ delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino");
+ Serial.println();
+
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+
+ // When using integer maths, calibration values that are supplied in floating point
+ // form need to be rescaled.
+ //
+ phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+ phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail just before the energy bucket is updated at the start
+ // of each new mains cycle.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone's value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1):
+ capacityOfEnergyBucket_long =
+ (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2;
+ IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled in a known way. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted);
+
+ long mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.println ("ADC mode: free-running");
+
+ // Set up the ADC to be free-running
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+ Serial.println ("----");
+
+// convertTemperature(); // start initial temperature conversion
+
+ Serial.print ("RF capability ");
+
+#ifdef RF_PRESENT
+ Serial.print ("IS present, freq = ");
+ if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); }
+ if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); }
+ rf12_initialize(nodeID, freq, networkGroup); // initialize RF
+#else
+ Serial.println ("is NOT present");
+#endif
+
+// convertTemperature(); // start initial temperature conversion
+}
+
+/* None of the workload in loop() is time-critical. All the processing of
+ * ADC data is done within the ISR.
+ */
+void loop()
+{
+// unsigned long timeNow = millis();
+ static byte perSecondTimer = 0;
+//
+ // The ISR provides a 50 Hz 'tick' which the main code is free to use.
+ if (newMainsCycle)
+ {
+ newMainsCycle = false;
+ perSecondTimer++;
+
+ if(perSecondTimer >= CYCLES_PER_SECOND)
+ {
+ perSecondTimer = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // Clear the accumulators for diverted energy. These are the "genuine"
+ // accumulators that are used by ISR rather than the copies that are
+ // regularly made available for use by the main code.
+ //
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR
+ }
+ }
+
+ if (datalogEventPending)
+ {
+ datalogEventPending= false;
+ tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod;
+ tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve)
+ tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh;
+ tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod));
+// tx_data.temperature_times100 = readTemperature();
+
+#ifdef RF_PRESENT
+ send_rf_data();
+#endif
+
+ Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts);
+ Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh);
+ Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100);
+// Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100);
+ Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle);
+ Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod);
+ Serial.println(')');
+// delay(POST_DATALOG_EVENT_DELAY_MILLIS);
+// convertTemperature(); // for use next time around
+ }
+
+/*
+ // occasional delays should not affect the operation of this revised code structure.
+ if (timeNow - timeAtLastDelay > 1000)
+ {
+ delay(100);
+ Serial.println("100ms delay");
+ timeAtLastDelay = timeNow;
+ }
+*/
+}
+
+
+ISR(ADC_vect)
+/*
+ * This Interrupt Service Routine looks after the acquisition and processing of
+ * raw samples from the ADC sub-processor. By means of various helper functions, all of
+ * the time-critical activities are processed within the ISR. The main code is notified
+ * by means of a flag when fresh copies of loggable data are available.
+ */
+{
+ static unsigned char sample_index = 0;
+ int rawSample;
+ long sampleIminusDC;
+ long phaseShiftedSampleVminusDC;
+ long filtV_div4;
+ long filtI_div4;
+ long instP;
+ long inst_Vsquared;
+
+ switch(sample_index)
+ {
+ case 0:
+ rawSample = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way
+ sample_index++; // increment the control flag
+ //
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long;
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+ //
+ checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle
+ //
+ // for the Vrms calculation (for datalogging only)
+ filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
+ inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12)
+ inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC)
+ sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC)
+ sampleSetsDuringThisDatalogPeriod++;
+ //
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+ sampleSetsDuringThisCycle++; // for real power calculations
+ refreshDisplay();
+ break;
+ case 1:
+ rawSample = ADC; // store the ADC value (this one is for Grid Current)
+ ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way
+ sample_index++; // increment the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+
+ sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ case 2:
+ rawSample = ADC; // store the ADC value (this one is for Diverted Current)
+ ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way
+ sample_index = 0; // reset the control flag
+ //
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8;
+ //
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+ phaseShiftedSampleVminusDC = lastSampleVminusDC_long
+ + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ //
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+ break;
+ default:
+ sample_index = 0; // to prevent lockup (should never get here)
+ }
+}
+
+/* -----------------------------------------------------------
+ * Start of various helper functions which are used by the ISR
+ */
+
+void checkProgress()
+/*
+ * This routine is called by the ISR when each voltage sample becomes available.
+ * At the start of each new mains cycle, another helper function is called.
+ * All other processing is done within this function.
+ */
+{
+// static enum loadStates nextStateOfLoad = LOAD_OFF;
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ // The start of a new mains cycle, just after the +ve going zero-crossing point.
+
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; }
+
+ processLatestContribution(); // for activities at the start of each new mains cycle
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_forEnergyBucket = 0;
+ sumP_atSupplyPoint;
+ sumP_diverted = 0;
+ sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle
+ sampleSetsDuringThisDatalogPeriod = 0;
+ // can't say "Go!" here 'cos we're in an ISR!
+ }
+ }
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ // check to see whether the trigger device can now be reliably armed
+ //
+ if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle
+ {
+
+ if (beyondStartUpPhase)
+ {
+ /* Determining whether any of the loads need to be changed is is a 3-stage process:
+ * - change the LOGICAL load states as necessary to maintain the energy level
+ * - update the PHYSICAL load states according to the logical -> physical mapping
+ * - update the driver lines for each of the loads.
+ */
+
+ // Restrictions apply for the period immediately after a load has been switched.
+ // Here the recentTransition flag is checked and updated as necessary.
+ if (recentTransition)
+ {
+ postTransitionCount++;
+ if (postTransitionCount >= POST_TRANSITION_MAX_COUNT)
+ {
+ recentTransition = false;
+ }
+ }
+
+ if (energyInBucket_long > midPointOfEnergyBucket_long)
+ {
+ // the energy state is in the upper half of the working range
+ lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_long > upperEnergyThreshold)
+ {
+ // Because the energy level is high, some action may be required
+ boolean OK_toAddLoad = true;
+ byte tempLoad = nextLogicalLoadToBeAdded();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now OFF has been identified for potentially being switched ON
+ if (recentTransition)
+ {
+ // During the post-transition period, any increase in the energy level is noted.
+ if (energyInBucket_long > upperEnergyThreshold)
+ {
+ upperEnergyThreshold = energyInBucket_long;
+
+ // the energy thresholds must remain within range
+ if (upperEnergyThreshold > capacityOfEnergyBucket_long)
+ {
+ upperEnergyThreshold = capacityOfEnergyBucket_long;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toAddLoad = false;
+ }
+ }
+
+ if (OK_toAddLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_ON;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ }
+ }
+ }
+ }
+ else
+ { // the energy state is in the lower half of the working range
+ upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold
+ if (energyInBucket_long < lowerEnergyThreshold)
+ {
+ // Because the energy level is low, some action may be required
+ boolean OK_toRemoveLoad = true;
+ byte tempLoad = nextLogicalLoadToBeRemoved();
+ if (tempLoad < noOfDumploads)
+ {
+ // a load which is now ON has been identified for potentially being switched OFF
+ if (recentTransition)
+ {
+ // During the post-transition period, any decrease in the energy level is noted.
+ if (energyInBucket_long < lowerEnergyThreshold)
+ {
+ lowerEnergyThreshold = energyInBucket_long;
+
+ // the energy thresholds must remain within range
+ if (lowerEnergyThreshold < 0)
+ {
+ lowerEnergyThreshold = 0;
+ }
+ }
+
+ // Only the active load may be switched during this period. All other loads must
+ // wait until the recent transition has had sufficient opportunity to take effect.
+ if (tempLoad != activeLoad)
+ {
+ OK_toRemoveLoad = false;
+ }
+ }
+
+ if (OK_toRemoveLoad)
+ {
+ logicalLoadState[tempLoad] = LOAD_OFF;
+ activeLoad = tempLoad;
+ postTransitionCount = 0;
+ recentTransition = true;
+ }
+ }
+ }
+ }
+
+ updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed
+
+ // update each of the physical loads
+ digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger
+ digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load
+
+ // update the Energy Diversion Detector
+ if (physicalLoadState[0] == LOAD_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+
+ // Now that the energy-related decisions have been taken, min and max limits can now
+ // be applied to the level of the energy bucket. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+ }
+ }
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ // The portion which is fed back into the integrator is approximately one percent
+ // of the average offset of all the Vsamples in the previous mains cycle.
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12);
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 230V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+// checkOutputModeSelection(); // updates outputMode if the external switch is in use
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is negative
+} // end of checkProgress()
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count > PERSISTENCE_FOR_POLARITY_CHANGE)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+void processLatestContribution()
+/*
+ * This routine runs once per mains cycle. It forms part of the ISR.
+ */
+{
+ newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code
+
+ // For the mechanism which controls the diversion of surplus power, the AVERAGE power
+ // at the 'grid' point during the previous mains cycle must be quantified. The first
+ // stage in this process is for the sum of all instantaneous power values to be divided
+ // by the number of sample sets that have contributed to its value. A similar operation
+ // is required for the diverted power data.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle;
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle;
+ //
+ // The per-mainsCycle variables can now be reset for ongoing use
+ sampleSetsDuringThisCycle = 0;
+ sumP_forEnergyBucket = 0;
+ sumP_diverted = 0;
+
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Average power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // their actual values in Joules.
+ //
+ long realEnergy_for_energyBucket = realPower_for_energyBucket;
+ long realEnergy_diverted = realPower_diverted;
+
+ // The latest energy contribution from the grid connection point can now be added
+ // to the energy bucket which determines the state of the dump-load.
+ //
+ energyInBucket_long += realEnergy_for_energyBucket;
+ energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Apply max and min limits to the bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision. To avoid the displayed
+ // value from creeping, any small contributions which are likely to be
+ // caused by noise are ignored.
+ //
+ if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) {
+ divertedEnergyRecent_IEU += realEnergy_diverted; }
+
+ // Whole Watt-Hours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ /* At the end of each datalogging period, copies are made of the relevant variables
+ * for use by the main code. These variable are then reset for use during the next
+ * datalogging period.
+ */
+ cycleCountForDatalogging ++;
+ if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES )
+ {
+ cycleCountForDatalogging = 0;
+
+ copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint;
+ copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh;
+ copyOf_sum_Vsquared = sum_Vsquared;
+ copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only)
+ copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only)
+
+ sumP_atSupplyPoint = 0;
+ sum_Vsquared = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ sampleSetsDuringThisDatalogPeriod = 0;
+ datalogEventPending = true;
+ }
+}
+
+byte nextLogicalLoadToBeAdded()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+ for (byte index = 0; index < noOfDumploads && !success; index++)
+ {
+ if (logicalLoadState[index] == LOAD_OFF)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+byte nextLogicalLoadToBeRemoved()
+{
+ byte retVal = noOfDumploads;
+ boolean success = false;
+
+ // this index counter can't be a 'byte' because the loop would run forever!
+ //
+ for (int index = (noOfDumploads -1); index >= 0 && !success; index--)
+ {
+ if (logicalLoadState[index] == LOAD_ON)
+ {
+ success = true;
+ retVal = index;
+ }
+ }
+ return(retVal);
+}
+
+
+void updatePhysicalLoadStates()
+/*
+ * This function provides the link between the logical and physical loads. The
+ * array, logicalLoadState[], contains the on/off state of all logical loads, with
+ * element 0 being for the one with the highest priority. The array,
+ * physicalLoadState[], contains the on/off state of all physical loads.
+ *
+ * The association between the physical and logical loads is 1:1. By default, numerical
+ * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set
+ * to have priority, rather than physical load 0, the logical-to-physical association for
+ * loads 0 and 1 are swapped.
+ *
+ * Any other mapping relaionships could be configured here.
+ */
+{
+ for (int i = 0; i < noOfDumploads; i++)
+ {
+ physicalLoadState[i] = logicalLoadState[i];
+ }
+
+ if (loadPriorityMode == LOAD_1_HAS_PRIORITY)
+ {
+ // swap physical loads 0 & 1 if remote load has priority
+ physicalLoadState[0] = logicalLoadState[1];
+ physicalLoadState[1] = logicalLoadState[0];
+ }
+}
+
+
+
+
+/* End of helper functions which are used by the ISR
+ * -------------------------------------------------
+ */
+
+/*
+this function changes the value of outputMode if the external switch is in use for this purpose
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState;
+ pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+*/
+
+/*
+// this function changes the value of the load priorities if the state of the external switch is altered
+void checkLoadPrioritySelection()
+{
+ static byte loadPrioritySwitchCount = 0;
+ int pinState = digitalRead(loadPrioritySelectorPin);
+ if (pinState != loadPriorityMode)
+ {
+ loadPrioritySwitchCount++;
+ }
+ if (loadPrioritySwitchCount >= 20)
+ {
+ loadPrioritySwitchCount = 0;
+ loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable
+ Serial.print ("loadPriority selection changed to ");
+ if (loadPriorityMode == LOAD_0_HAS_PRIORITY) {
+ Serial.println ( "load 0"); }
+ else {
+ Serial.println ( "load 1"); }
+ }
+}
+*/
+void configureParamsForSelectedOutputMode()
+/*
+ * retained for compatibility with previous versions
+ */
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerThreshold_default =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperThreshold_default =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerThreshold_default = capacityOfEnergyBucket_long * 0.5;
+ upperThreshold_default = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold = ");
+ Serial.println(lowerThreshold_default);
+ Serial.print(" upperEnergyThreshold = ");
+ Serial.println(upperThreshold_default);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+}
+
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+} // end of refreshDisplay()
+
+/*
+ * void convertTemperature()
+{
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(CONVERT_TEMPERATURE);
+}
+
+int readTemperature()
+{
+ byte buf[9];
+ int result;
+
+ oneWire.reset();
+ oneWire.write(SKIP_ROM);
+ oneWire.write(READ_SCRATCHPAD);
+ for(int i=0; i<9; i++) buf[i]=oneWire.read();
+ if(oneWire.crc8(buf,8)==buf[8])
+ {
+ result=(buf[1]<<8)|buf[0];
+ // result is temperature x16, multiply by 6.25 to convert to temperature x100
+ result=(result*6)+(result>>2);
+ }
+ else result=BAD_TEMPERATURE;
+ return result;
+}
+*/
+
+#ifdef RF_PRESENT
+void send_rf_data()
+//
+// To avoid disturbance to the sampling process, the RFM12B needs to remain in its
+// active state rather than being periodically put to sleep.
+{
+ // check whether it's ready to send, and an exit route if it gets stuck
+ int i = 0;
+ while (!rf12_canSend() && i<10)
+ {
+ rf12_recvDone();
+ i++;
+ }
+ rf12_sendNow(0, &tx_data, sizeof tx_data);
+}
+#endif
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino
new file mode 100644
index 0000000..e0ce267
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino
@@ -0,0 +1,1093 @@
+/* Mk2_bothDisplays_1.ino
+ *
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * It is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * February 2014
+ */
+
+#include
+
+#include
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// The two versions of the hardware require different logic. The following line should
+// be included if the additional logic chips are present, or excluded if they are
+// absent (in which case some wire links need to be fitted)
+//
+//#define PIN_SAVING_HARDWARE
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+// allocation of digital pins for prototype PCB-based rig (with simple display adapter)
+// ******************************************************
+// D0 & D1 are reserved for the Serial i/f
+// D2 is a driver line for the 4-digit display (segment D, via series resistor)
+const byte outputModeSelectorPin = 3; // <-- with the internal pullup
+const byte outputForTrigger = 4;
+// D5 is a driver line for the 4-digit display (segment B, via series resistor)
+// D6 is a driver line for the 4-digit display (digit 3, via wire link)
+// D7 is a driver line for the 4-digit display (digit 2, via wire link)
+// D8 is a driver line for the 4-digit display (segment F, via series resistor)
+// D9 is a driver line for the 4-digit display (segment A, via series resistor)
+// D10 is a driver line for the 4-digit display (segment DP, via series resistor)
+// D11 is a driver line for the 4-digit display (segment C, via series resistor)
+// D12 is a driver line for the 4-digit display (segment G, via series resistor)
+// D13 is a driver line for the 4-digit display (digit 4, via wire link)
+
+// allocation of analogue pins
+// ***************************
+// A0 (D14) is a driver line for the 4-digit display (digit 1, via wire link)
+// A1 (D15) is a driver line for the 4-digit display (segment E, via series resistor)
+// A2 (D16) is unused (it's routed to pin 1 of IC4 which is not fitted)
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long cycleCount = 0; // counts mains cycles from start-up
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+// The two versions of the hardware require different logic.
+#ifdef PIN_SAVING_HARDWARE
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+#else // PIN_SAVING_HARDWARE
+
+#define ON HIGH
+#define OFF LOW
+
+const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point
+enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED};
+
+byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11};
+byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14};
+
+// The final column of segMap[] is for the decimal point status. In this version,
+// the decimal point is treated just like all the other segments, so there is
+// no need to access this column specifically.
+//
+byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = {
+ ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0
+ OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1
+ ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2
+ ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3
+ OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4
+ ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5
+ ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6
+ ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7
+ ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8
+ ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9
+ ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10
+ OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11
+ ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12
+ ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13
+ OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14
+ ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15
+ ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16
+ ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17
+ ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18
+ ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11
+};
+#endif // PIN_SAVING_HARDWARE
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy divertion detection
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_bothDisplays_1.ino");
+ Serial.println();
+
+#ifdef PIN_SAVING_HARDWARE
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // control lines for the 74HC4543 7-seg display driver and the DP line
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+
+ // control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+#else
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ pinMode(segmentDrivePin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ pinMode(digitSelectorPin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); }
+
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ digitalWrite(segmentDrivePin[i], OFF); }
+#endif
+
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+
+ Serial.println ("----");
+
+#ifdef WORKLOAD_CHECK
+ Serial.println ("WELCOME TO WORKLOAD_CHECK ");
+
+// <<- start of commented out section, to save on RAM space!
+/*
+ Serial.println (" This mode of operation allows the spare processing capacity of the system");
+ Serial.println ("to be analysed. Additional delay is gradually increased until all spare time");
+ Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. ");
+ Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is ");
+ Serial.println ("checked several times before the delay is increased. ");
+ */
+// <<- end of commented out section, to save on RAM space!
+
+ Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, ");
+ Serial.println ("that is available for doing additional processing.");
+ Serial.println ();
+ #endif
+}
+
+// An Interrupt Service Routine is now defined in which the ADC is instructed to
+// measure V and I alternately. A "data ready"flag is set after each voltage conversion
+// has been completed.
+// For each pair of samples, this means that current is measured before voltage. The
+// current sample is taken first because the phase of the waveform for current is generally
+// slightly advanced relative to the waveform for voltage. The data ready flag is cleared
+// within loop().
+// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is
+// executed whenever the ADC timer expires. In this mode, the next ADC conversion is
+// initiated from within this ISR.
+//
+void timerIsr(void)
+{
+ static unsigned char sample_index = 0;
+
+ switch(sample_index)
+ {
+ case 0:
+ sampleV = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current
+ ADCSRA |= (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static int samplesDuringThisCycle; // for normalising the power in each mains cycle
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static enum polarities polarityOfLastSampleV; // for zero-crossing detection
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte timerForDisplayUpdate = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ enum polarities polarityNow;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+ cycleCount++;
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle)
+ {
+ realEnergy_diverted = 0;
+ }
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA)
+ { // the 4-digit display needs to be refreshed every few mS. For convenience,
+ // this action is performed every N times around this processing loop.
+ timerForDisplayUpdate = 0;
+
+/*
+// Need to comment this section out if WORKLOAD_CHECK is enabled
+ Serial.print("Diverted: " );
+ Serial.print(divertedEnergyTotal_Wh);
+ Serial.print(" Wh plus ");
+ Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU);
+
+ Serial.print(" J , EDD is" );
+*/
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+/*
+// Need to comment this section out if WORKLOAD_CHECK is enabled
+ if (EDD_isActive) {
+ Serial.println(" on" ); }
+ else {
+ Serial.println(" off" ); }
+*/
+ configureValueForDisplay();
+ }
+ else
+ {
+ timerForDisplayUpdate++;
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ samplesDuringThisCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (samplesDuringThisCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ samplesDuringThisCycle = 0;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ samplesDuringThisCycle++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+ // The two versions of the hardware require different logic.
+
+#ifdef PIN_SAVING_HARDWARE
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+
+#else // PIN_SAVING_HARDWARE
+
+ // This version is more straightforward because the digit-enable lines can be
+ // used to mask out all of the transitory states, including the Decimal Point.
+ // The sequence is:
+ //
+ // 1. de-activate the digit-enable line that was previously active
+ // 2. determine the next location which is to be active
+ // 3. determine the relevant character for the new active location
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ // 5. activate the digit-enable line for the new active location
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ displayTime_count = 0;
+
+ // 1. de-activate the location which is currently being displayed
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED);
+
+ // 2. determine the next digit location which is to be displayed
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 3. determine the relevant character for the new active location
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) {
+ byte segmentState = segMap[digitVal][segment];
+ digitalWrite( segmentDrivePin[segment], segmentState); }
+
+ // 5. activate the digit-enable line for the new active location
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED);
+ }
+#endif // PIN_SAVING_HARDWARE
+
+} // end of refreshDisplay()
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino
new file mode 100644
index 0000000..d912fa1
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino
@@ -0,0 +1,1096 @@
+/* Mk2_bothDisplays_2.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * It is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * September 2014
+ */
+
+#include
+#include
+
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// The two versions of the hardware require different logic. The following line should
+// be included if the additional logic chips are present, or excluded if they are
+// absent (in which case some wire links need to be fitted)
+//
+//#define PIN_SAVING_HARDWARE
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+// allocation of digital pins which are not dependent on the display type that is in use
+// *************************************************************************************
+const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup
+const byte outputForTrigger = 4; // <-- an output which is active-low
+
+// allocation of analogue pins which are not dependent on the display type that is in use
+// **************************************************************************************
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+// The two versions of the hardware require different logic.
+#ifdef PIN_SAVING_HARDWARE
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+#else // PIN_SAVING_HARDWARE
+
+#define ON HIGH
+#define OFF LOW
+
+const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point
+enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED};
+
+byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11};
+byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14};
+
+// The final column of segMap[] is for the decimal point status. In this version,
+// the decimal point is treated just like all the other segments, so there is
+// no need to access this column specifically.
+//
+byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = {
+ ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0
+ OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1
+ ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2
+ ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3
+ OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4
+ ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5
+ ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6
+ ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7
+ ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8
+ ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9
+ ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10
+ OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11
+ ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12
+ ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13
+ OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14
+ ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15
+ ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16
+ ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17
+ ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18
+ ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11
+};
+#endif // PIN_SAVING_HARDWARE
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy divertion detection
+long requiredExportPerMainsCycle_inIEU;
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_bothDisplays_2.ino");
+ Serial.println();
+
+#ifdef PIN_SAVING_HARDWARE
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+#else
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ pinMode(segmentDrivePin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ pinMode(digitSelectorPin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); }
+
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ digitalWrite(segmentDrivePin[i], OFF); }
+#endif
+
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1<>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+ configureParamsForSelectedOutputMode();
+
+ Serial.println ("----");
+
+#ifdef WORKLOAD_CHECK
+ Serial.println ("WELCOME TO WORKLOAD_CHECK ");
+
+// <<- start of commented out section, to save on RAM space!
+/*
+ Serial.println (" This mode of operation allows the spare processing capacity of the system");
+ Serial.println ("to be analysed. Additional delay is gradually increased until all spare time");
+ Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. ");
+ Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is ");
+ Serial.println ("checked several times before the delay is increased. ");
+ */
+// <<- end of commented out section, to save on RAM space!
+
+ Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, ");
+ Serial.println ("that is available for doing additional processing.");
+ Serial.println ();
+ #endif
+}
+
+// An Interrupt Service Routine is now defined in which the ADC is instructed to
+// measure V and I alternately. A "data ready"flag is set after each voltage conversion
+// has been completed.
+// For each pair of samples, this means that current is measured before voltage. The
+// current sample is taken first because the phase of the waveform for current is generally
+// slightly advanced relative to the waveform for voltage. The data ready flag is cleared
+// within loop().
+// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is
+// executed whenever the ADC timer expires. In this mode, the next ADC conversion is
+// initiated from within this ISR.
+//
+void timerIsr(void)
+{
+ static unsigned char sample_index = 0;
+
+ switch(sample_index)
+ {
+ case 0:
+ sampleV = ADC; // store the ADC value (this one is for Voltage)
+ ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current
+ ADCSRA |= (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static int samplesDuringThisCycle; // for normalising the power in each mains cycle
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static enum polarities polarityOfLastSampleV; // for zero-crossing detection
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte timerForDisplayUpdate = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ enum polarities polarityNow;
+ if(sampleVminusDC_long > 0) {
+ polarityNow = POSITIVE; }
+ else {
+ polarityNow = NEGATIVE; }
+
+ if (polarityNow == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts
+
+ realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle)
+ {
+ realEnergy_diverted = 0;
+ }
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA)
+ { // the 4-digit display needs to be refreshed every few mS. For convenience,
+ // this action is performed every N times around this processing loop.
+ timerForDisplayUpdate = 0;
+
+/*
+// Need to comment this section out if WORKLOAD_CHECK is enabled
+ Serial.print("Diverted: " );
+ Serial.print(divertedEnergyTotal_Wh);
+ Serial.print(" Wh plus ");
+ Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU);
+
+ Serial.print(" J , EDD is" );
+*/
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+/*
+// Need to comment this section out if WORKLOAD_CHECK is enabled
+ if (EDD_isActive) {
+ Serial.println(" on" ); }
+ else {
+ Serial.println(" off" ); }
+*/
+ configureValueForDisplay();
+ }
+ else
+ {
+ timerForDisplayUpdate++;
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ samplesDuringThisCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (samplesDuringThisCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ samplesDuringThisCycle = 0;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ samplesDuringThisCycle++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+ // The two versions of the hardware require different logic.
+
+#ifdef PIN_SAVING_HARDWARE
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+
+#else // PIN_SAVING_HARDWARE
+
+ // This version is more straightforward because the digit-enable lines can be
+ // used to mask out all of the transitory states, including the Decimal Point.
+ // The sequence is:
+ //
+ // 1. de-activate the digit-enable line that was previously active
+ // 2. determine the next location which is to be active
+ // 3. determine the relevant character for the new active location
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ // 5. activate the digit-enable line for the new active location
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ displayTime_count = 0;
+
+ // 1. de-activate the location which is currently being displayed
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED);
+
+ // 2. determine the next digit location which is to be displayed
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 3. determine the relevant character for the new active location
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) {
+ byte segmentState = segMap[digitVal][segment];
+ digitalWrite( segmentDrivePin[segment], segmentState); }
+
+ // 5. activate the digit-enable line for the new active location
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED);
+ }
+#endif // PIN_SAVING_HARDWARE
+
+} // end of refreshDisplay()
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino
new file mode 100644
index 0000000..586b040
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino
@@ -0,0 +1,1128 @@
+/* Mk2_bothDisplays_2.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * It is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * September 2014
+ */
+
+#include
+#include
+
+#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time)
+
+// Physical constants, please do not change!
+#define SECONDS_PER_MINUTE 60
+#define MINUTES_PER_HOUR 60
+#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules)
+
+// Change these values to suit the local mains frequency and supply meter
+#define CYCLES_PER_SECOND 50
+#define SWEETZONE_IN_JOULES 3600
+#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator
+
+// to prevent the diverted energy total from 'creeping'
+#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0)
+long antiCreepLimit_inIEUperMainsCycle;
+
+// The two versions of the hardware require different logic. The following line should
+// be included if the additional logic chips are present, or excluded if they are
+// absent (in which case some wire links need to be fitted)
+//
+#define PIN_SAVING_HARDWARE
+
+// definition of enumerated types
+enum polarities {NEGATIVE, POSITIVE};
+enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low
+enum outputModes {ANTI_FLICKER, NORMAL};
+
+// ---------------- Extra Features selection ----------------------
+//
+// - WORKLOAD_CHECK, for determining how much spare processing time there is.
+//
+// #define WORKLOAD_CHECK // <-- Include this line is this feature is required
+
+
+// The power-diversion logic can operate in either of two modes:
+//
+// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level.
+// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations
+// of the local mains voltage.
+//
+// The output mode is determined in realtime via a selector switch
+enum outputModes outputMode;
+
+// allocation of digital pins which are not dependent on the display type that is in use
+// *************************************************************************************
+const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup
+const byte outputForTrigger = 4; // <-- an output which is active-low
+
+// allocation of analogue pins which are not dependent on the display type that is in use
+// **************************************************************************************
+const byte voltageSensor = 3; // A3 is for the voltage sensor
+const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current
+const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current
+
+const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle
+const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale
+
+// General global variables that are used in multiple blocks so cannot be static.
+// For integer maths, many variables need to be 'long'
+//
+boolean beyondStartUpPhase = false; // start-up delay, allows things to settle
+long triggerThreshold_long; // for determining when the trigger may be safely armed
+long energyInBucket_long; // in Integer Energy Units
+long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size.
+long lowerEnergyThreshold_long; // for turning triac off
+long upperEnergyThreshold_long; // for turning triac on
+// int phaseCal_grid_int; // to avoid the need for floating-point maths
+// int phaseCal_diverted_int; // to avoid the need for floating-point maths
+long DCoffset_V_long; // <--- for LPF
+long DCoffset_V_min; // <--- for LPF
+long DCoffset_V_max; // <--- for LPF
+long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range
+unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range
+long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size.
+
+unsigned long displayShutdown_inMainsCycles;
+unsigned long absenceOfDivertedEnergyCount = 0;
+long mainsCyclesPerHour;
+
+// this setting is only used if anti-flicker mode is enabled
+float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5
+
+// for interaction between the main processor and the ISRs
+volatile boolean dataReady = false;
+int sampleI_grid;
+int sampleI_diverted;
+int sampleV;
+
+// For an enhanced polarity detection mechanism, which includes a persistence check
+#define POLARITY_CHECK_MAXCOUNT 2 // sample sets
+enum polarities polarityOfMostRecentVsample;
+enum polarities polarityConfirmed;
+enum polarities polarityConfirmedOfLastSampleV;
+
+// For a mechanism to check the continuity of the sampling sequence
+#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles
+int sampleCount_forContinuityChecker;
+int sampleSetsDuringThisMainsCycle;
+int lowestNoOfSampleSetsPerMainsCycle;
+
+// Calibration values
+//-------------------
+// Two calibration values are used: powerCal and phaseCal.
+// With most hardware, the default values are likely to work fine without
+// need for change. A full explanation of each of these values now follows:
+//
+// powerCal is a floating point variable which is used for converting the
+// product of voltage and current samples into Watts.
+//
+// The correct value of powerCal is dependent on the hardware that is
+// in use. For best resolution, the hardware should be configured so that the
+// voltage and current waveforms each span most of the ADC's usable range. For
+// many systems, the maximum power that will need to be measured is around 3kW.
+//
+// My sketch "MinAndMaxValues.ino" provides a good starting point for
+// system setup. First arrange for the CT to be clipped around either core of a
+// cable which supplies a suitable load; then run the tool. The resulting values
+// should sit nicely within the range 0-1023. To allow some room for safety,
+// a margin of around 100 levels should be left at either end. This gives a
+// output range of around 800 ADC levels, which is 80% of its usable range.
+//
+// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
+// voltage and current waveforms. This provides an easy way for the user to be
+// confident that their system has been set up correctly for the power levels
+// that are to be measured.
+//
+// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
+// The purpose of each input sensor is to convert the measured parameter into a
+// low-voltage signal which fits nicely within the ADC's input range.
+//
+// In the case of 240V mains voltage, the numerical value of the input signal
+// in Volts is likely to be fairly similar to the output signal in ADC levels.
+// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
+// output range. Stated more formally, the conversion rate of the overall system
+// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
+//
+// In the case of AC current, however, the situation is very different. At
+// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
+// has a peak-to-peak range of 35A. This is smaller than the output signal by
+// around a factor of twenty. The conversion rate of the overall system for
+// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
+//
+// When calculating "real power", which is what this code does, the individual
+// conversion rates for voltage and current are not of importance. It is
+// only the conversion rate for POWER which is important. This is the
+// product of the individual conversion rates for voltage and current. It
+// therefore has the units of ADC-steps squared per Watt. Most systems will
+// have a power conversion rate of around 20 (ADC-steps squared per Watt).
+//
+// powerCal is the RECIPR0CAL of the power conversion rate. A good value
+// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
+//
+const float powerCal_grid = 0.0435; // for CT1
+const float powerCal_diverted = 0.0435; // for CT2
+
+
+// phaseCal is used to alter the phase of the voltage waveform relative to the
+// current waveform. The algorithm interpolates between the most recent pair
+// of voltage samples according to the value of phaseCal.
+//
+// With phaseCal = 1, the most recent sample is used.
+// With phaseCal = 0, the previous sample is used
+// With phaseCal = 0.5, the mid-point (average) value in used
+//
+// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
+// and are not recommended. By altering the order in which V and I samples are
+// taken, and for how many loops they are stored, it should always be possible to
+// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When
+// measuring a resistive load, the voltage and current waveforms should be perfectly
+// aligned. In this situation, the Power Factor will be 1.
+//
+// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
+// value of phaseCal for any hardware configuration. An index of my various Mk2-related
+// exhibits is available at http://openenergymonitor.org/emon/node/1757
+//
+//const float phaseCal_grid = 1.0; <--- not used in this version
+//const float phaseCal_diverted = 1.0; <--- not used in this version
+
+// Various settings for the 4-digit display, which needs to be refreshed every few mS
+const byte noOfDigitLocations = 4;
+const byte noOfPossibleCharacters = 22;
+#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates
+#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles
+#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity
+// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds
+
+// The two versions of the hardware require different logic.
+#ifdef PIN_SAVING_HARDWARE
+
+#define DRIVER_CHIP_DISABLED HIGH
+#define DRIVER_CHIP_ENABLED LOW
+
+// the primary segments are controlled by a pair of logic chips
+const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver
+const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer
+
+byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP)
+byte decimalPointLine = 14; // <- this line has to be individually controlled.
+
+byte digitLocationLine[noOfDigitLocationLines] = {16,15};
+byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6};
+
+// The final column of digitValueMap[] is for the decimal point status. In this version,
+// the decimal point has to be treated differently than the other seven segments, so
+// a convenient means of accessing this column is provided.
+//
+byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = {
+ LOW , LOW , LOW , LOW , LOW , // '0' <- element 0
+ LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1
+ LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2
+ LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3
+ LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4
+ LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5
+ LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6
+ LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7
+ HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8
+ HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9
+ LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10
+ LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11
+ LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12
+ LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13
+ LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14
+ LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15
+ LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16
+ LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17
+ HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18
+ HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19
+ HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20
+ HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21
+};
+
+// a tidy means of identifying the DP status data when accessing the above table
+const byte DPstatus_columnID = noOfDigitSelectionLines;
+
+byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = {
+ LOW , LOW , // Digit 1
+ LOW , HIGH, // Digit 2
+ HIGH, LOW , // Digit 3
+ HIGH, HIGH, // Digit 4
+};
+
+#else // PIN_SAVING_HARDWARE
+
+#define ON HIGH
+#define OFF LOW
+
+const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point
+enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED};
+
+byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11};
+byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14};
+
+// The final column of segMap[] is for the decimal point status. In this version,
+// the decimal point is treated just like all the other segments, so there is
+// no need to access this column specifically.
+//
+byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = {
+ ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0
+ OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1
+ ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2
+ ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3
+ OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4
+ ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5
+ ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6
+ ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7
+ ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8
+ ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9
+ ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10
+ OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11
+ ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12
+ ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13
+ OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14
+ ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15
+ ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16
+ ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17
+ ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18
+ ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20
+ OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11
+};
+#endif // PIN_SAVING_HARDWARE
+
+byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank
+
+boolean EDD_isActive = false; // energy divertion detection
+long requiredExportPerMainsCycle_inIEU;
+
+
+void setup()
+{
+ pinMode(outputForTrigger, OUTPUT);
+ digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low
+
+ pinMode(outputModeSelectorPin, INPUT);
+ digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor
+ delay (100); // allow time to settle
+ int pinState = digitalRead(outputModeSelectorPin); // initial selection and
+ outputMode = (enum outputModes)pinState; // assignment of output mode
+
+ delay(5000); // allow time to open Serial monitor
+
+ Serial.begin(9600);
+ Serial.println();
+ Serial.println("-------------------------------------");
+ Serial.println("Sketch ID: Mk2_bothDisplays_3.ino");
+ Serial.println();
+
+#ifdef PIN_SAVING_HARDWARE
+ // configure the IO drivers for the 4-digit display
+ //
+ // the Decimal Point line is driven directly from the processor
+ pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line
+
+ // set up the control lines for the 74HC4543 7-seg display driver
+ for (int i = 0; i < noOfDigitSelectionLines; i++) {
+ pinMode(digitSelectionLine[i], OUTPUT); }
+
+ // an enable line is required for the 74HC4543 7-seg display driver
+ pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // set up the control lines for the 74HC138 2->4 demux
+ for (int i = 0; i < noOfDigitLocationLines; i++) {
+ pinMode(digitLocationLine[i], OUTPUT); }
+#else
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ pinMode(segmentDrivePin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ pinMode(digitSelectorPin[i], OUTPUT); }
+
+ for (int i = 0; i < noOfDigitLocations; i++) {
+ digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); }
+
+ for (int i = 0; i < noOfSegmentsPerDigit; i++) {
+ digitalWrite(segmentDrivePin[i], OFF); }
+#endif
+
+
+
+ // When using integer maths, calibration values that have supplied in floating point
+ // form need to be rescaled.
+ //
+// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths
+// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths
+
+ // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
+ // scaling of the energy detection mechanism that is in use. This avoids the need
+ // to re-scale every energy contribution, thus saving processing time. This process
+ // is described in more detail in the function, allGeneralProcessing(), just before
+ // the energy bucket is updated at the start of each new cycle of the mains.
+ //
+ // An electricity meter has a small range over which energy can ebb and flow without
+ // penalty. This has been termed its "sweet-zone". For optimal performance, the energy
+ // bucket of a PV Router should match this value. The sweet-zone value is therefore
+ // included in the calculation below.
+ //
+ // For the flow of energy at the 'grid' connection point (CT1)
+ capacityOfEnergyBucket_long =
+ (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid);
+ energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up
+
+ // For recording the accumulated amount of diverted energy data (using CT2), a similar
+ // calibration mechanism is required. Rather than a bucket with a fixed capacity, the
+ // accumulator for diverted energy just needs to be scaled correctly. As soon as its
+ // value exceeds 1 Wh, an associated WattHour register is incremented, and the
+ // accumulator's value is decremented accordingly. The calculation below is to determine
+ // the scaling for this accumulator.
+
+ IEU_per_Wh =
+ (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted);
+
+ // to avoid the diverted energy accumulator 'creeping' when the load is not active
+ antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid);
+
+ mainsCyclesPerHour = (long)CYCLES_PER_SECOND *
+ SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
+
+ displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour;
+
+ requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid);
+
+
+ // Define operating limits for the LP filter which identifies DC offset in the voltage
+ // sample stream. By limiting the output range, the filter always should start up
+ // correctly.
+ DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
+ DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
+ DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
+
+ Serial.print ("ADC mode: ");
+ Serial.print (ADC_TIMER_PERIOD);
+ Serial.println ( " uS fixed timer");
+
+ // Set up the ADC to be triggered by a hardware timer of fixed duration
+ ADCSRA = (1< 50)
+ {
+ count = 0;
+ del++; // increase delay by 1uS
+ }
+ }
+#endif
+
+ } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks!
+
+#ifdef WORKLOAD_CHECK
+ switch (displayFlag)
+ {
+ case 0: // the result is available now, but don't display until the next loop
+ displayFlag++;
+ break;
+ case 1: // with minimum delay, it's OK to print now
+ Serial.print(res);
+ displayFlag++;
+ break;
+ case 2: // with minimum delay, it's OK to print now
+ Serial.println("uS");
+ displayFlag++;
+ break;
+ default:; // for most of the time, displayFlag is 3
+ }
+#endif
+
+} // end of loop()
+
+
+// This routine is called to process each set of V & I samples. The main processor and
+// the ADC work autonomously, their operation being only linked via the dataReady flag.
+// As soon as a new set of data is made available by the ADC, the main processor can
+// start to work on it immediately.
+//
+void allGeneralProcessing()
+{
+ static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half)
+ static long sumP_grid; // for per-cycle summation of 'real power'
+ static long sumP_diverted; // for per-cycle summation of 'real power'
+ static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage)
+ static long lastSampleVminusDC_long; // for the phaseCal algorithm
+ static byte timerForDisplayUpdate = 0;
+ static enum triacStates nextStateOfTriac = TRIAC_OFF;
+
+ // remove DC offset from the raw voltage sample by subtracting the accurate value
+ // as determined by a LP filter.
+ long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
+
+ // determine the polarity of the latest voltage sample
+ if(sampleVminusDC_long > 0) {
+ polarityOfMostRecentVsample = POSITIVE; }
+ else {
+ polarityOfMostRecentVsample = NEGATIVE; }
+ confirmPolarity();
+
+ if (polarityConfirmed == POSITIVE)
+ {
+ if (beyondStartUpPhase)
+ {
+ if (polarityConfirmedOfLastSampleV != POSITIVE)
+ {
+ // This is the start of a new +ve half cycle (just after the zero-crossing point)
+ // a simple routine for checking the performance of this new ISR structure
+ if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) {
+ lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; }
+
+ triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle
+
+ // Calculate the real power and energy during the last whole mains cycle.
+ //
+ // sumP contains the sum of many individual calculations of instantaneous power. In
+ // order to obtain the average power during the relevant period, sumP must first be
+ // divided by the number of samples that have contributed to its value.
+ //
+ // The next stage would normally be to apply a calibration factor so that real power
+ // can be expressed in Watts. That's fine for floating point maths, but it's not such
+ // a good idea when integer maths is being used. To keep the numbers large, and also
+ // to save time, calibration of power is omitted at this stage. Real Power (stored as
+ // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts.
+ //
+ long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts
+ long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts
+
+ realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation
+
+ // Next, the energy content of this power rating needs to be determined. Energy is
+ // power multiplied by time, so the next step is normally to multiply the measured
+ // value of power by the time over which it was measured.
+ // Instanstaneous power is calculated once every mains cycle. When integer maths is
+ // being used, a repetitive power-to-energy conversion seems an unnecessary workload.
+ // As all sampling periods are of similar duration, it is more efficient simply to
+ // add all of the power samples together, and note that their sum is actually
+ // CYCLES_PER_SECOND greater than it would otherwise be.
+ // Although the numerical value itself does not change, I thought that a new name
+ // may be helpful so as to minimise confusion.
+ // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than
+ // the actual energy in Joules.
+ //
+ long realEnergy_grid = realPower_grid;
+ long realEnergy_diverted = realPower_diverted;
+
+
+ // Energy contributions from the grid connection point (CT1) are summed in an
+ // accumulator which is known as the energy bucket. The purpose of the energy bucket
+ // is to mimic the operation of the supply meter. The range over which energy can
+ // pass to and fro without loss or charge to the user is known as its 'sweet-zone'.
+ // The capacity of the energy bucket is set to this same value within setup().
+ //
+ // The latest contribution can now be added to this energy bucket
+ energyInBucket_long += realEnergy_grid;
+
+ // Apply max and min limits to bucket's level. This is to ensure correct operation
+ // when conditions change, i.e. when import changes to export, and vici versa.
+ //
+ if (energyInBucket_long > capacityOfEnergyBucket_long) {
+ energyInBucket_long = capacityOfEnergyBucket_long; }
+ else
+ if (energyInBucket_long < 0) {
+ energyInBucket_long = 0; }
+
+ if (EDD_isActive) // Energy Diversion Display
+ {
+ // For diverted energy, the latest contribution needs to be added to an
+ // accumulator which operates with maximum precision.
+
+ if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle)
+ {
+ realEnergy_diverted = 0;
+ }
+
+ divertedEnergyRecent_IEU += realEnergy_diverted;
+
+ // Whole kWhours are then recorded separately
+ if (divertedEnergyRecent_IEU > IEU_per_Wh)
+ {
+ divertedEnergyRecent_IEU -= IEU_per_Wh;
+ divertedEnergyTotal_Wh++;
+ }
+ }
+
+ if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA)
+ { // the 4-digit display needs to be refreshed every few mS. For convenience,
+ // this action is performed every N times around this processing loop.
+ timerForDisplayUpdate = 0;
+
+ // After a pre-defined period of inactivity, the 4-digit display needs to
+ // close down in readiness for the next's day's data.
+ //
+ if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles)
+ {
+ // clear the accumulators for diverted energy
+ divertedEnergyTotal_Wh = 0;
+ divertedEnergyRecent_IEU = 0;
+ EDD_isActive = false; // energy diversion detector is now inactive
+ }
+
+ configureValueForDisplay();
+ }
+ else
+ {
+ timerForDisplayUpdate++;
+ }
+
+ // continuity checker
+ sampleCount_forContinuityChecker++;
+ if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT)
+ {
+ sampleCount_forContinuityChecker = 0;
+ Serial.println(lowestNoOfSampleSetsPerMainsCycle);
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ }
+
+ // clear the per-cycle accumulators for use in this new mains cycle.
+ sampleSetsDuringThisMainsCycle = 0;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+
+ } // end of processing that is specific to the first Vsample in each +ve half cycle
+
+ // still processing samples where the voltage is POSITIVE ...
+ if (triggerNeedsToBeArmed == true)
+ {
+ // check to see whether the trigger device can now be reliably armed
+ if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level
+ {
+ if (energyInBucket_long < lowerEnergyThreshold_long) {
+ // when below the lower threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_OFF; }
+ else
+ if (energyInBucket_long > upperEnergyThreshold_long) {
+ // when above the upper threshold, always set the triac to "off"
+ nextStateOfTriac = TRIAC_ON; }
+ else {
+ // otherwise, leave the triac's state unchanged (hysteresis)
+ }
+
+ // set the Arduino's output pin accordingly, and clear the flag
+ digitalWrite(outputForTrigger, nextStateOfTriac);
+ triggerNeedsToBeArmed = false;
+
+ // update the Energy Diversion Detector
+ if (nextStateOfTriac == TRIAC_ON) {
+ absenceOfDivertedEnergyCount = 0;
+ EDD_isActive = true; }
+ else {
+ absenceOfDivertedEnergyCount++; }
+ }
+ }
+ }
+ else
+ {
+ // wait until the DC-blocking filters have had time to settle
+ if(millis() > startUpPeriod * 1000)
+ {
+ beyondStartUpPhase = true;
+ sumP_grid = 0;
+ sumP_diverted = 0;
+ sampleSetsDuringThisMainsCycle = 0;
+ sampleCount_forContinuityChecker = 0;
+ lowestNoOfSampleSetsPerMainsCycle = 999;
+ Serial.println ("Go!");
+ }
+ }
+
+ } // end of processing that is specific to samples where the voltage is positive
+
+ else // the polatity of this sample is negative
+ {
+ if (polarityConfirmedOfLastSampleV != NEGATIVE)
+ {
+ // This is the start of a new -ve half cycle (just after the zero-crossing point)
+ // which is a convenient point to update the Low Pass Filter for DC-offset removal
+ //
+ long previousOffset = DCoffset_V_long;
+ DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01
+ cumVdeltasThisCycle_long = 0;
+
+ // To ensure that the LPF will always start up correctly when 240V AC is available, its
+ // output value needs to be prevented from drifting beyond the likely range of the
+ // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds.
+ //
+ if (DCoffset_V_long < DCoffset_V_min) {
+ DCoffset_V_long = DCoffset_V_min; }
+ else
+ if (DCoffset_V_long > DCoffset_V_max) {
+ DCoffset_V_long = DCoffset_V_max; }
+
+ checkOutputModeSelection(); // updates outputMode if switch is changed
+
+ } // end of processing that is specific to the first Vsample in each -ve half cycle
+ } // end of processing that is specific to samples where the voltage is positive
+
+ // processing for EVERY pair of samples
+ //
+ // First, deal with the power at the grid connection point (as measured via CT1)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the grid current waveform
+// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8);
+ long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6)
+ long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ // Now deal with the diverted power (as measured via CT2)
+ // remove most of the DC offset from the current sample (the precise value does not matter)
+ long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8;
+
+ // phase-shift the voltage waveform so that it aligns with the diverted current waveform
+// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long
+// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8);
+ long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when
+ // phaseCal is not in use
+
+ // calculate the "real power" in this sample pair and add to the accumulated sum
+ filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6)
+ instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12)
+ instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
+ sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
+
+ sampleSetsDuringThisMainsCycle++;
+
+ // store items for use during next loop
+ cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
+ lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
+ polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries
+
+ refreshDisplay();
+}
+// ----- end of main Mk2i code -----
+
+void confirmPolarity()
+{
+ /* This routine prevents a zero-crossing point from being declared until
+ * a certain number of consecutive samples in the 'other' half of the
+ * waveform have been encountered.
+ */
+ static byte count = 0;
+ if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) {
+ count++; }
+ else {
+ count = 0; }
+
+ if (count >= POLARITY_CHECK_MAXCOUNT)
+ {
+ count = 0;
+ polarityConfirmed = polarityOfMostRecentVsample;
+ }
+}
+
+
+
+// this function changes the value of outputMode if the state of the external switch is altered
+void checkOutputModeSelection()
+{
+ static byte count = 0;
+ int pinState = digitalRead(outputModeSelectorPin);
+ if (pinState != outputMode)
+ {
+ count++;
+ }
+ if (count >= 20)
+ {
+ count = 0;
+ outputMode = (enum outputModes)pinState; // change the global variable
+ Serial.print ("outputMode selection changed to ");
+ if (outputMode == NORMAL) {
+ Serial.println ( "normal"); }
+ else {
+ Serial.println ( "anti-flicker"); }
+
+ configureParamsForSelectedOutputMode();
+ }
+}
+
+
+void configureParamsForSelectedOutputMode()
+{
+ if (outputMode == ANTI_FLICKER)
+ {
+ // settings for anti-flicker mode
+ lowerEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode);
+ upperEnergyThreshold_long =
+ capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode);
+ }
+ else
+ {
+ // settings for normal mode
+ lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5;
+ }
+
+ // display relevant settings for selected output mode
+ Serial.print(" capacityOfEnergyBucket_long = ");
+ Serial.println(capacityOfEnergyBucket_long);
+ Serial.print(" lowerEnergyThreshold_long = ");
+ Serial.println(lowerEnergyThreshold_long);
+ Serial.print(" upperEnergyThreshold_long = ");
+ Serial.println(upperEnergyThreshold_long);
+
+ Serial.print(">>free RAM = ");
+ Serial.println(freeRam()); // a useful value to keep an eye on
+}
+
+// called infrequently, to update the characters to be displayed
+void configureValueForDisplay()
+{
+ static byte locationOfDot = 0;
+
+// Serial.println(divertedEnergyTotal_Wh);
+
+ if (EDD_isActive)
+ {
+ unsigned int val = divertedEnergyTotal_Wh;
+ boolean energyValueExceeds10kWh;
+
+ if (val < 10000) {
+ // no need to re-scale (display to 3 DPs)
+ energyValueExceeds10kWh = false; }
+ else {
+ // re-scale is needed (display to 2 DPs)
+ energyValueExceeds10kWh = true;
+ val = val/10; }
+
+ byte thisDigit = val / 1000;
+ charsForDisplay[0] = thisDigit;
+ val -= 1000 * thisDigit;
+
+ thisDigit = val / 100;
+ charsForDisplay[1] = thisDigit;
+ val -= 100 * thisDigit;
+
+ thisDigit = val / 10;
+ charsForDisplay[2] = thisDigit;
+ val -= 10 * thisDigit;
+
+ charsForDisplay[3] = val;
+
+ // assign the decimal point location
+ if (energyValueExceeds10kWh) {
+ charsForDisplay[1] += 10; } // dec point after 2nd digit
+ else {
+ charsForDisplay[0] += 10; } // dec point after 1st digit
+ }
+ else
+ {
+ // "walking dots" display
+ charsForDisplay[locationOfDot] = 20; // blank
+
+ locationOfDot++;
+ if (locationOfDot >= noOfDigitLocations) {
+ locationOfDot = 0; }
+
+ charsForDisplay[locationOfDot] = 21; // dot
+ }
+/*
+ Serial.print(charsForDisplay[0]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[1]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[2]);
+ Serial.print(" ");
+ Serial.print(charsForDisplay[3]);
+ Serial.println();
+*/
+// valueToBeDisplayed++;
+}
+
+void refreshDisplay()
+{
+ // This routine keeps track of which digit is being displayed and checks when its
+ // display time has expired. It then makes the necessary adjustments for displaying
+ // the next digit.
+ // The two versions of the hardware require different logic.
+
+#ifdef PIN_SAVING_HARDWARE
+ // With this version of the hardware, care must be taken that all transitory states
+ // are masked out. Note that the enableDisableLine only masks the seven primary
+ // segments, not the Decimal Point line which must therefore be treated separately.
+ // The sequence is:
+ //
+ // 1. set the decimal point line to 'off'
+ // 2. disable the 7-segment driver chip
+ // 3. determine the next location which is to be active
+ // 4. set up the location lines for the new active location
+ // 5. determine the relevant character for the new active location
+ // 6. configure the driver chip for the new character to be displayed
+ // 7. set up decimal point line for the new active location
+ // 8. enable the 7-segment driver chip
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ byte lineState;
+
+ displayTime_count = 0;
+
+ // 1. disable the Decimal Point driver line;
+ digitalWrite( decimalPointLine, LOW);
+
+ // 2. disable the driver chip while changes are taking place
+ digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED);
+
+ // 3. determine the next digit location to be active
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 4. set up the digit location drivers for the new active location
+ for (byte line = 0; line < noOfDigitLocationLines; line++) {
+ lineState = digitLocationMap[digitLocationThatIsActive][line];
+ digitalWrite( digitLocationLine[line], lineState); }
+
+ // 5. determine the character to be displayed at this new location
+ // (which includes the decimal point information)
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 6. configure the 7-segment driver for the character to be displayed
+ for (byte line = 0; line < noOfDigitSelectionLines; line++) {
+ lineState = digitValueMap[digitVal][line];
+ digitalWrite( digitSelectionLine[line], lineState); }
+
+ // 7. set up the Decimal Point driver line;
+ digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]);
+
+ // 8. enable the 7-segment driver chip
+ digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED);
+ }
+
+#else // PIN_SAVING_HARDWARE
+
+ // This version is more straightforward because the digit-enable lines can be
+ // used to mask out all of the transitory states, including the Decimal Point.
+ // The sequence is:
+ //
+ // 1. de-activate the digit-enable line that was previously active
+ // 2. determine the next location which is to be active
+ // 3. determine the relevant character for the new active location
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ // 5. activate the digit-enable line for the new active location
+
+ static byte displayTime_count = 0;
+ static byte digitLocationThatIsActive = 0;
+
+ displayTime_count++;
+
+ if (displayTime_count > MAX_DISPLAY_TIME_COUNT)
+ {
+ displayTime_count = 0;
+
+ // 1. de-activate the location which is currently being displayed
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED);
+
+ // 2. determine the next digit location which is to be displayed
+ digitLocationThatIsActive++;
+ if (digitLocationThatIsActive >= noOfDigitLocations) {
+ digitLocationThatIsActive = 0; }
+
+ // 3. determine the relevant character for the new active location
+ byte digitVal = charsForDisplay[digitLocationThatIsActive];
+
+ // 4. set up the segment drivers for the character to be displayed (includes the DP)
+ for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) {
+ byte segmentState = segMap[digitVal][segment];
+ digitalWrite( segmentDrivePin[segment], segmentState); }
+
+ // 5. activate the digit-enable line for the new active location
+ digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED);
+ }
+#endif // PIN_SAVING_HARDWARE
+
+} // end of refreshDisplay()
+
+int freeRam () {
+ extern int __heap_start, *__brkval;
+ int v;
+ return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
+}
+
+
+
+
diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino
new file mode 100644
index 0000000..f905283
--- /dev/null
+++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino
@@ -0,0 +1,1133 @@
+/* Mk2_bothDisplays_3a.ino
+ *
+ * (initially released as Mk2_bothDisplays_1 in March 2014)
+ * This sketch is for diverting surplus PV power to a dump load using a triac.
+ * It is based on the Mk2i PV Router code that I have posted in on the
+ * OpenEnergyMonitor forum. The original version, and other related material,
+ * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757
+ *
+ * In this latest version, the pin-allocations have been changed to suit my
+ * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is
+ * fed from one of the secondary coils of the transformer. Current is measured
+ * via Current Transformers at the CT1 and CT2 ports.
+ *
+ * CT1 is for 'grid' current, to be measured at the grid supply point.
+ * CT2 is for the load current, so that diverted energy can be recorded
+ *
+ * A persistence-based 4-digit display is supported. This can be driven in two
+ * different ways, one with an extra pair of logic chips, and one without. The
+ * appropriate version of the sketch must be selected by including or commenting
+ * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code.
+ *
+ * September 2014: renamed as Mk2_bothDisplays_2, with these changes:
+ * - cycleCount removed (was not actually used in this sketch, but could have overflowed);
+ * - removal of unhelpful comments in the IO pin section;
+ * - tidier initialisation of display logic in setup();
+ * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility);
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3, with these changes:
+ * - persistence check added for zero-crossing detection (polarityConfirmed)
+ * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances
+ *
+ * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed.
+ *
+ * Robin Emley
+ * www.Mk2PVrouter.co.uk
+ * December 2014
+ */
+
+#include
+#include