diff --git a/mos.yml b/mos.yml index fae6bc19..35ca7c57 100644 --- a/mos.yml +++ b/mos.yml @@ -135,6 +135,12 @@ config_schema: - ["gains.bwgain", "f", 4194304, {title: ""}] - ["gains.phcalb", "f", 0, {title: ""}] + - ["scales", "o", {title: "", abstract: true}] + - ["scales.voltage_scale", "f", 1, {title: ""}] + - ["scales.current_scale", "f", 1, {title: ""}] + - ["scales.aenergy_scale", "f", 1, {title: ""}] + - ["scales.apower_scale", "f", 1, {title: ""}] + build_vars: # BLE disabled for most models. MGOS_HAP_BLE: 0 @@ -700,6 +706,7 @@ conds: name: Plus1PMMini sources: - src/ShellyMini1PMGen3 + - src/BL0942 libs: - location: https://github.com/mongoose-os-libs/mongoose build_vars: @@ -721,6 +728,8 @@ conds: PRODUCT_HW_REV: "0.1.0" STOCK_FW_MODEL: Plus1PMMini MAX_NUM_HAP_SESSIONS: 16 + MGOS_CONFIG_DEV_6: "shelly" + MGOS_CONFIG_DEV_7: "shelly,4096" config_schema: - ["device.id", "ShellyPlus1PMMini-??????"] - ["shelly.name", "ShellyPlus1PMMini-??????"] @@ -734,11 +743,21 @@ conds: - ["gdo1.name", "Garage Door"] - ["gdo1.open_sensor_mode", 2] + - ["factory", "o", {title: ""}] + - ["factory.batch", "s", "", {title: ""}] + - ["factory.model", "s", "", {title: ""}] + - ["factory.version", "i", 0, {title: ""}] + - ["factory.calib", "o", {title: "Factory calib settings"}] + - ["factory.calib.done", "b", false, {title: ""}] + - ["factory.calib.scales0", "scales", {title: ""}] + - when: build_vars.MODEL == "ShellyMini1PMGen3" apply: name: Mini1PMG3 libs: - location: https://github.com/mongoose-os-libs/mongoose + sources: + - src/BL0942 build_vars: OTA_DATA_ADDR: 0x10000 OTA_DATA_SIZE: 0x4000 @@ -763,10 +782,12 @@ conds: PRODUCT_HW_REV: "0.1.0" STOCK_FW_MODEL: Mini1PMG3 MAX_NUM_HAP_SESSIONS: 16 + MGOS_CONFIG_DEV_6: "shelly" + MGOS_CONFIG_DEV_7: "shelly,4096" config_schema: - - ["device.id", "ShellyMini1PMG3-??????"] - - ["shelly.name", "ShellyMini1PMG3-??????"] - - ["wifi.ap.ssid", "ShellyMini1PMG3-??????"] + - ["device.id", "Shelly1PMMiniG3-????????????"] + - ["shelly.name", "Shelly1PMMiniG3-????????????"] + - ["wifi.ap.ssid", "Shelly1PMMiniG3-????????????"] - ["sw1", "sw", {title: "SW1 settings"}] - ["sw1.name", "Shelly SW"] - ["in1", "in", {title: "Input 1 settings"}] @@ -776,6 +797,14 @@ conds: - ["gdo1.name", "Garage Door"] - ["gdo1.open_sensor_mode", 2] + - ["factory", "o", {title: ""}] + - ["factory.batch", "s", "", {title: ""}] + - ["factory.model", "s", "", {title: ""}] + - ["factory.version", "i", 0, {title: ""}] + - ["factory.calib", "o", {title: "Factory calib settings"}] + - ["factory.calib.done", "b", false, {title: ""}] + - ["factory.calib.scales0", "scales", {title: ""}] + - when: build_vars.MODEL == "ShellyMini1Gen3" apply: name: Mini1G3 diff --git a/src/BL0942/shelly_pm_bl0942.cpp b/src/BL0942/shelly_pm_bl0942.cpp new file mode 100644 index 00000000..1ce59656 --- /dev/null +++ b/src/BL0942/shelly_pm_bl0942.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shelly_pm_bl0942.hpp" + +#include + +#include "mgos.hpp" + +struct packet { + uint8_t frame_header; + uint8_t i_rms[3]; + uint8_t v_rms[3]; + uint8_t i_fast_rms[3]; + uint8_t watt[3]; + uint8_t cf_cnt[3]; + uint8_t frequency[2]; + uint8_t reserved1; + uint8_t status; + uint8_t reserved2; + uint8_t reserved3; + uint8_t checksum; +} __attribute__((packed)); + +namespace shelly { + +BL0942PowerMeter::BL0942PowerMeter(int id, int tx_pin, int rx_pin, + int meas_time, int uart_no) + : PowerMeter(id), + tx_pin_(tx_pin), + rx_pin_(rx_pin), + meas_time_(meas_time), + uart_no_(uart_no), + meas_timer_(std::bind(&BL0942PowerMeter::MeasureTimerCB, this)) { +} + +BL0942PowerMeter::~BL0942PowerMeter() { +} + +#define BL_READ 0x58 +#define BL_WRITE 0xA8 + +#define BL_SOFT_RESET 0x1C +#define BL_USR_WRPROT 0x1D +#define BL_MODE 0x19 +#define BL_TPS_CTRL 0x1B +#define BL_I_FAST_RMS_CTRL 0x10 + +#define BL_ADDR 0x0 + +#define BL_WATT 0x6 + +Status BL0942PowerMeter::Init() { + if (rx_pin_ < 0 && tx_pin_ < 0) { + return mgos::Errorf(STATUS_INVALID_ARGUMENT, "no valid pins"); + } + + struct mgos_uart_config ucfg; + mgos_uart_config_set_defaults(uart_no_, &ucfg); + + ucfg.baud_rate = 9600; + + ucfg.dev.rx_gpio = rx_pin_; + ucfg.dev.tx_gpio = tx_pin_; + ucfg.dev.cts_gpio = -1; + ucfg.dev.rts_gpio = -1; + + if (!mgos_uart_configure(uart_no_, &ucfg)) { + return mgos::Errorf(STATUS_INVALID_ARGUMENT, "Failed to configure UART"); + } + + mgos_uart_set_rx_enabled(uart_no_, true); + + meas_timer_.Reset(meas_time_ * 1000, MGOS_TIMER_REPEAT); + LOG(LL_INFO, ("BL0942 @ %d/%d", rx_pin_, tx_pin_)); + + this->WriteReg(BL_SOFT_RESET, 0x5a5a5a); + // this->WriteReg(BL_USR_WRPROT, 0x550000); + // this->WriteReg(BL_MODE, 0x001000); + // this->WriteReg(BL_TPS_CTRL, 0xFF4700); + // this->WriteReg(BL_I_FAST_RMS_CTRL, 0x1C1800); + + return Status::OK(); +} + +StatusOr BL0942PowerMeter::GetPowerW() { + return apa_; +} + +StatusOr BL0942PowerMeter::GetEnergyWH() { + return aea_; +} + +bool BL0942PowerMeter::WriteReg(uint8_t reg, uint32_t val) { + uint8_t tx_buf[6] = {BL_WRITE | BL_ADDR, + reg, + (uint8_t) ((val >> 16) & 0xFF), + (uint8_t) ((val >> 8) & 0xFF), + (uint8_t) ((val >> 0) & 0xFF), + 0}; + + for (int i = 0; i < 5; i++) { + tx_buf[5] += tx_buf[i]; + } + tx_buf[5] = tx_buf[5] ^ 0xFF; + mgos_uart_write(uart_no_, tx_buf, 6); + mgos_uart_flush(uart_no_); + mgos_msleep(1); + return true; +} + +bool BL0942PowerMeter::ReadReg(uint8_t reg, uint8_t *rx_buf, size_t len) { + bool whole_packet = (len == 23); + uint8_t tx_buf[2] = {BL_READ | BL_ADDR, reg}; + mgos_uart_write(uart_no_, tx_buf, 2); + mgos_uart_flush(uart_no_); + + // Delay to allow data to be available + int baud = 9600; + mgos_msleep(roundf(len * 8 / baud) * 1e3); + + int read_len = mgos_uart_read(uart_no_, rx_buf, len); + + uint8_t chksum = + tx_buf[0] + + (whole_packet ? 0 : tx_buf[1]); // ignore tx_buf[1] when reading packet + for (int i = 0; i < len - 1; i++) { + chksum += rx_buf[i]; + // LOG(LL_INFO, ("data %i:%02X", i, rx_buf[i])); + } + chksum ^= 0xFF; + + if (read_len != len || rx_buf[len - 1] != chksum) { + LOG(LL_ERROR, ("wrong checksum")); + return false; + } + return true; +} + +uint32_t convert_le24(uint8_t v[3]) { + return ((uint32_t) v[2] << 16) | (uint32_t) (v[1] << 8) | v[0]; +} + +uint32_t convert_le16(uint8_t v[2]) { + return (uint32_t) (v[1] << 8) | v[0]; +} + +void BL0942PowerMeter::MeasureTimerCB() { + packet rx_buf; + static uint32_t cf_cnt = 0; + + if (this->ReadReg(0xAA, (uint8_t *) &rx_buf, sizeof(rx_buf))) { + if (rx_buf.frame_header == 0x55) { + uint32_t cf = convert_le24(rx_buf.cf_cnt); + cf = (cf_cnt & 0xFF000000) | cf; + if (cf_cnt > cf) { + cf += 0x1000000; + } + cf_cnt = cf; + + float wref = (3537 / (1.218 * 1.218 * 4)); + float vref = (73989 / (1.218 * 4)); + float iref = (305978 / (1.218)); + + float vo = convert_le24(rx_buf.v_rms) / vref; + float vi = convert_le24(rx_buf.i_rms) / iref; + int32_t wa_tmp = convert_le24(rx_buf.watt); + if (wa_tmp & 0x800000) { + wa_tmp |= 0xFF000000; + } + float wa = wa_tmp / wref; + float fr = 1000000.0 / (float) convert_le16(rx_buf.frequency); + + apa_ = wa; + aea_ = cf / (wref * 3600 / (1638.4 * 256)); + + LOG(LL_INFO, ("vo: %.1f wa: %.2f i: %.2f fr: %.2f ae: %.2f", vo, wa, vi, + fr, aea_)); + } + } +} + +} // namespace shelly diff --git a/src/BL0942/shelly_pm_bl0942.hpp b/src/BL0942/shelly_pm_bl0942.hpp new file mode 100644 index 00000000..900e785a --- /dev/null +++ b/src/BL0942/shelly_pm_bl0942.hpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shelly_pm.hpp" + +#include "mgos_timers.hpp" + +namespace shelly { + +class BL0942PowerMeter : public PowerMeter { + public: + BL0942PowerMeter(int id, int tx_pin, int rx_pin, int meas_time, int uart_no); + virtual ~BL0942PowerMeter(); + + Status Init() override; + StatusOr GetPowerW() override; + StatusOr GetEnergyWH() override; + + private: + void MeasureTimerCB(); + + const int tx_pin_, rx_pin_, meas_time_, uart_no_; + + float apa_ = 0; // Last active power reading, W. + float aea_ = 0; // Accumulated active energy, Wh. + // + bool ReadReg(uint8_t reg, uint8_t *rx_buf, size_t len); + bool WriteReg(uint8_t reg, uint32_t val); + + mgos::Timer meas_timer_; +}; + +} // namespace shelly diff --git a/src/ShellyMini1PMGen3/shelly_init.cpp b/src/ShellyMini1PMGen3/shelly_init.cpp index eee13c0b..113f88ff 100644 --- a/src/ShellyMini1PMGen3/shelly_init.cpp +++ b/src/ShellyMini1PMGen3/shelly_init.cpp @@ -19,6 +19,7 @@ #include "shelly_hap_input.hpp" #include "shelly_input_pin.hpp" #include "shelly_main.hpp" +#include "shelly_pm_bl0942.hpp" #include "shelly_sys_led_btn.hpp" #include "shelly_temp_sensor_ntc.hpp" @@ -35,28 +36,37 @@ void CreatePeripherals(std::vector> *inputs, in->Init(); inputs->emplace_back(in); -// not yet compatible #ifdef MGOS_HAVE_ADC sys_temp->reset(new TempSensorSDNT1608X103F3950(3, 3.3f, 10000.0f)); #endif - // std::unique_ptr pm() + std::unique_ptr pm(new BL0942PowerMeter(1, 6, 7, 1, 1)); // BL0942 GPIO6 TX GPIO7 RX - // const Status &st = pm->Init(); - // if (st.ok()) { - // pms->emplace_back(std::move(pm)); - // } else { - // const std::string &s = st.ToString(); - // LOG(LL_ERROR, ("PM init failed: %s", s.c_str())); - // } + const Status &st = pm->Init(); + if (st.ok()) { + pms->emplace_back(std::move(pm)); + } else { + const std::string &s = st.ToString(); + LOG(LL_ERROR, ("PM init failed: %s", s.c_str())); + } InitSysLED(LED_GPIO, LED_ON); InitSysBtn(BTN_GPIO, BTN_DOWN); } +void PrintCalibrationData() { + mgos_config_factory *c = &(mgos_sys_config.factory); + LOG(LL_INFO, ("calibration.done %Q", c->calib.done)); + mgos_config_scales *s = &c->calib.scales0; + LOG(LL_INFO, ("gains vs: %f cs: %f ps: %f es: %f", s->voltage_scale, + s->current_scale, s->apower_scale, s->aenergy_scale)); +} + void CreateComponents(std::vector> *comps, std::vector> *accs, HAPAccessoryServerRef *svr) { + void PrintCalibrationData(); + bool gdo_mode = mgos_sys_config_get_shelly_mode() == (int) Mode::kGarageDoor; if (gdo_mode) { hap::CreateHAPGDO(1, FindInput(1), FindInput(2), FindOutput(1),