Skip to content

Instantly share code, notes, and snippets.

@mathieucarbou
Last active January 17, 2024 13:35
Show Gist options
  • Save mathieucarbou/93f6caae6edd306f3a75bc1d849bf197 to your computer and use it in GitHub Desktop.
Save mathieucarbou/93f6caae6edd306f3a75bc1d849bf197 to your computer and use it in GitHub Desktop.
MycilaJSY - Arduino / ESP32 lib for JSY-MK-194

JSY-MK-194 lib compatible with ESP32 / Arduino dual cores.

You will need to adapt it to your needs.

It works in async and non-sync mode.

If the bauds rate is not set to

How to use:

#include "MycilaJSY.h"

// load from a config system
bool Mycila::JSYConfigClass::isEnabled() { return Mycila::Config.getBool(KEY_JSY_ENABLE); }
int32_t Mycila::JSYConfigClass::getRXPin() { return Mycila::Config.getInt(KEY_JSY_RX_PIN); }
int32_t Mycila::JSYConfigClass::getTXPin() { return Mycila::Config.getInt(KEY_JSY_TX_PIN); }
The MIT License (MIT)
---------------------
Copyright © 2023-2024, Mathieu Carbou
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.
// SPDX-License-Identifier: MIT
/*
* Copyright (C) 2023-2024 Mathieu Carbou and others
*/
#include "MycilaJSY.h"
#include "MycilaLogger.h"
#include <algorithm>
#define TAG "JSY"
#define FLUSH_BUFFER_SIZE 256
static const uint8_t JSY_READ_MSG[] = {0x01, 0x03, 0x00, 0x48, 0x00, 0x0E, 0x44, 0x18};
static uint8_t BUFFER[FLUSH_BUFFER_SIZE];
void Mycila::JSYClass::begin() {
if (_enabled)
return;
if (!JSYConfig.isEnabled()) {
Logger.warn(TAG, "Disable JSY");
return;
}
int32_t valRX = JSYConfig.getRXPin();
if (GPIO_IS_VALID_GPIO(valRX)) {
_pinRX = (gpio_num_t)valRX;
} else {
Logger.error(TAG, "Disable JSY: Invalid JSY RX pin: %u", _pinRX);
_pinRX = GPIO_NUM_NC;
return;
}
int32_t valTX = JSYConfig.getTXPin();
if (GPIO_IS_VALID_OUTPUT_GPIO(valTX)) {
_pinTX = (gpio_num_t)valTX;
} else {
Logger.error(TAG, "Disable JSY: Invalid JSY TX pin: %u", _pinTX);
_pinTX = GPIO_NUM_NC;
return;
}
Logger.info(TAG, "Enable JSY...");
Logger.debug(TAG, "- JSY RX Pin: %u", _pinRX);
Logger.debug(TAG, "- JSY TX Pin: %u", _pinTX);
Logger.debug(TAG, "- Async: %s", _async ? "true" : "false");
const JSYBaudRate baudRates[] = {JSYBaudRate::BAUD_4800, JSYBaudRate::BAUD_9600, JSYBaudRate::BAUD_19200, JSYBaudRate::BAUD_38400};
bool ok = false;
for (int i = 0; i < 4; i++) {
_serial->begin((uint32_t)baudRates[i], SERIAL_8N1, _pinTX, _pinRX);
while (!_serial)
yield();
if (_read(4)) {
Logger.debug(TAG, "- JSY Speed Detected: %u bps", (uint32_t)baudRates[i]);
if (baudRates[i] == _baudRate) {
ok = true;
break;
}
_setBaudRate(_baudRate);
_serial->end();
_serial->begin((uint32_t)_baudRate, SERIAL_8N1, _pinTX, _pinRX);
while (!_serial)
yield();
if (!_read(4)) {
Logger.error(TAG, "Unable to read JSY data after baud rate change");
break;
}
ok = true;
break;
} else {
_serial->end();
}
}
if (!ok) {
Logger.error(TAG, "Unable to read JSY data with any baud rate");
_serial->end();
return;
}
if (_async && xTaskCreateUniversal(_task, "Mycila-JSY", MYCILA_JSY_ASYNC_STACK_SIZE, this, 1, nullptr, MYCILA_JSY_ASYNC_CORE) != pdPASS) {
Logger.error(TAG, "Unable to create JSY async task");
return;
}
_enabled = true;
}
void Mycila::JSYClass::end() {
if (_disable()) {
_serial->end();
}
}
void Mycila::JSYClass::endAndResetEnergy() {
_disable();
Logger.warn(TAG, "Energy Reset...");
const uint8_t data[] = {0x01, 0x10, 0x00, 0x0C, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xFA};
_serial->write(data, 13);
_serial->flush();
_readSerial();
// Note: do not end() _serial: ESP needs to restart right after sending the reset command
Logger.debug(TAG, "Energy Reset done");
}
bool Mycila::JSYClass::read() {
if (!_enabled)
return false;
return _read();
}
void Mycila::JSYClass::toJson(const JsonObject& root) {
root["current1"] = current1;
root["current2"] = current2;
root["enabled"] = _enabled;
root["energy_returned1"] = energyReturned1;
root["energy_returned2"] = energyReturned2;
root["energy1"] = energy1;
root["energy2"] = energy2;
root["frequency"] = frequency;
root["power_factor1"] = powerFactor1;
root["power_factor2"] = powerFactor2;
root["power1"] = power1;
root["power2"] = power2;
root["voltage1"] = voltage1;
root["voltage2"] = voltage2;
}
bool Mycila::JSYClass::_read(uint8_t maxCount) {
while (maxCount > 0 && !_read()) {
if (--maxCount == 0)
return false;
delay(60);
}
return true;
}
bool __attribute__((hot)) Mycila::JSYClass::_read() {
if (_reading)
return false;
_reading = true;
_serial->write(JSY_READ_MSG, 8);
_serial->flush();
size_t count = _readSerial();
if (count != 61 || BUFFER[0] != 0x01) {
_reading = false;
return false;
}
voltage1 = ((BUFFER[3] << 24) + (BUFFER[4] << 16) + (BUFFER[5] << 8) + BUFFER[6]) * 0.0001;
current1 = ((BUFFER[7] << 24) + (BUFFER[8] << 16) + (BUFFER[9] << 8) + BUFFER[10]) * 0.0001;
power1 = ((BUFFER[11] << 24) + (BUFFER[12] << 16) + (BUFFER[13] << 8) + BUFFER[14]) * (BUFFER[27] == 1 ? -0.0001 : 0.0001);
energy1 = ((BUFFER[15] << 24) + (BUFFER[16] << 16) + (BUFFER[17] << 8) + BUFFER[18]) * 0.0001;
powerFactor1 = ((BUFFER[19] << 24) + (BUFFER[20] << 16) + (BUFFER[21] << 8) + BUFFER[22]) * 0.001;
energyReturned1 = ((BUFFER[23] << 24) + (BUFFER[24] << 16) + (BUFFER[25] << 8) + BUFFER[26]) * 0.0001;
// BUFFER[27] is the sign of power1
// BUFFER[28] is the sign of power2
// BUFFER[29] unused
// BUFFER[30] unused
frequency = round(((BUFFER[31] << 24) + (BUFFER[32] << 16) + (BUFFER[33] << 8) + BUFFER[34]) * 0.01);
voltage2 = ((BUFFER[35] << 24) + (BUFFER[36] << 16) + (BUFFER[37] << 8) + BUFFER[38]) * 0.0001;
current2 = ((BUFFER[39] << 24) + (BUFFER[40] << 16) + (BUFFER[41] << 8) + BUFFER[42]) * 0.0001;
power2 = ((BUFFER[43] << 24) + (BUFFER[44] << 16) + (BUFFER[45] << 8) + BUFFER[46]) * (BUFFER[28] == 1 ? -0.0001 : 0.0001);
energy2 = ((BUFFER[47] << 24) + (BUFFER[48] << 16) + (BUFFER[49] << 8) + BUFFER[50]) * 0.0001;
powerFactor2 = ((BUFFER[51] << 24) + (BUFFER[52] << 16) + (BUFFER[53] << 8) + BUFFER[54]) * 0.001;
energyReturned2 = ((BUFFER[55] << 24) + (BUFFER[56] << 16) + (BUFFER[57] << 8) + BUFFER[58]) * 0.0001;
_reading = false;
return true;
}
bool Mycila::JSYClass::_setBaudRate(JSYBaudRate baudRate) {
Logger.debug(TAG, "Set JSY baud rate to %u bps...", (uint32_t)baudRate);
uint8_t data[] = {0x00, 0x10, 0x00, 0x04, 0x00, 0x01, 0x02, 0x01, 0x00, 0x00, 0x00};
switch (baudRate) {
case JSYBaudRate::BAUD_4800:
data[8] = 0x05;
data[9] = 0x6B;
data[10] = 0xD7;
break;
case JSYBaudRate::BAUD_9600:
data[8] = 0x06;
data[9] = 0x2B;
data[10] = 0xD6;
break;
case JSYBaudRate::BAUD_19200:
data[8] = 0x07;
data[9] = 0xEA;
data[10] = 0x16;
break;
case JSYBaudRate::BAUD_38400:
data[8] = 0x08;
data[9] = 0xAA;
data[10] = 0x12;
break;
default:
assert(false);
break;
}
_serial->write(data, 11);
_serial->flush();
_readSerial();
Logger.debug(TAG, "JSY baud rate updated.");
return true;
}
size_t Mycila::JSYClass::_readSerial() {
size_t count = 0;
while (const size_t n = _serial->available()) {
const size_t pos = count % FLUSH_BUFFER_SIZE;
count += _serial->readBytes(BUFFER + pos, min(n, FLUSH_BUFFER_SIZE - pos));
}
return count;
}
bool Mycila::JSYClass::_disable() {
if (_enabled) {
Logger.info(TAG, "Disable JSY...");
_enabled = false;
while (_reading)
delay(100);
current1 = 0;
current2 = 0;
energy1 = 0;
energy2 = 0;
energyReturned1 = 0;
energyReturned2 = 0;
frequency = 0;
power1 = 0;
power2 = 0;
powerFactor1 = 0;
powerFactor2 = 0;
voltage1 = 0;
voltage2 = 0;
return true;
}
return false;
}
void Mycila::JSYClass::_task(void* pvParameters) {
// Serial.println("JSY async task started");
// Serial.println(xPortGetCoreID());
JSYClass* jsy = reinterpret_cast<JSYClass*>(pvParameters);
while (jsy->_enabled) {
jsy->_read();
if (jsy->_enabled)
// https://esp32developer.com/programming-in-c-c/tasks/tasks-vs-co-routines
delay(max(portTICK_PERIOD_MS, static_cast<uint32_t>(MYCILA_JSY_ASYNC_READ_PAUSE_INTERVAL_MS)));
}
vTaskDelete(NULL);
}
namespace Mycila {
JSYClass JSY;
JSYConfigClass JSYConfig;
} // namespace Mycila
// SPDX-License-Identifier: MIT
/*
* Copyright (C) 2023-2024 Mathieu Carbou and others
*/
#pragma once
#include <ArduinoJson.h>
#include <esp32-hal-gpio.h>
#ifndef MYCILA_JSY_ASYNC_CORE
#define MYCILA_JSY_ASYNC_CORE 0
#endif
#ifndef MYCILA_JSY_ASYNC_STACK_SIZE
#define MYCILA_JSY_ASYNC_STACK_SIZE 256 * 5
#endif
#ifndef MYCILA_JSY_ASYNC_READ_PAUSE_INTERVAL_MS
#define MYCILA_JSY_ASYNC_READ_PAUSE_INTERVAL_MS 60
#endif
namespace Mycila {
enum class JSYBaudRate {
BAUD_4800 = 4800,
BAUD_9600 = 9600,
BAUD_19200 = 19200,
BAUD_38400 = 38400,
};
// class to be implemented in your application, to source the enablement and pins of the JSY from a configuration system
class JSYConfigClass {
public:
bool isEnabled();
int32_t getRXPin();
int32_t getTXPin();
};
class JSYClass {
public:
// Serial to be used, defaulted to Serial2
void setSerial(HardwareSerial* serial) { _serial = serial; }
// Async mode, defaulted to false.
// If yes, a task will be created to read the JSY in background.
void setAsync(bool async) { _async = async; }
// Baud rate to use, defaulted to 38400
void setBaudRate(JSYBaudRate baudRate) { _baudRate = baudRate; }
void begin();
void end();
// ends the JSY reading and resets
// note that after this call, I need to restart the ESP so that the reset can be achieved.
// TODO: check if we can instead do a voltage cycle (LOW - HIGH) on the pins and restart Serial2
void endAndResetEnergy();
gpio_num_t getRXPin() { return _pinRX; }
gpio_num_t getTXPin() { return _pinTX; }
bool isEnabled() { return _enabled; }
bool isAsync() { return _async; }
JSYBaudRate getBaudRate() { return _baudRate; }
HardwareSerial* getSerial() { return _serial; }
void toJson(const JsonObject& root);
bool read();
public:
volatile float current1 = 0; // A
volatile float current2 = 0; // A
volatile float energy1 = 0; // kWh
volatile float energy2 = 0; // kWh
volatile float energyReturned1 = 0; // kWh
volatile float energyReturned2 = 0; // kWh
volatile uint8_t frequency = 0; // Hz
volatile float power1 = 0; // W
volatile float power2 = 0; // W
volatile float powerFactor1 = 0;
volatile float powerFactor2 = 0;
volatile float voltage1 = 0; // V
volatile float voltage2 = 0; // V
private:
gpio_num_t _pinRX = GPIO_NUM_NC;
gpio_num_t _pinTX = GPIO_NUM_NC;
HardwareSerial* _serial = &Serial2;
JSYBaudRate _baudRate = JSYBaudRate::BAUD_38400;
volatile bool _async = false;
volatile bool _enabled = false;
volatile bool _reading = false;
private:
bool _disable();
bool _read();
bool _read(uint8_t maxCount);
bool _setBaudRate(JSYBaudRate baudRate);
size_t _readSerial();
static void _task(void* pvParameters);
};
extern JSYClass JSY;
extern JSYConfigClass JSYConfig;
} // namespace Mycila
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment