diff --git a/CHANGELOG.md b/CHANGELOG.md index 335d3c0..c2db1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. ## 2026-02 +- Mark YS-IRTM scripts as production; add all 6 to manifest; remove Under Development banner from README +- Add JK200 BMS MODBUS-RTU reader (`the_pill/MODBUS/JK200-MBS`) with README +- Mark Deye SG02LP1 MODBUS-RTU scripts as production; fix @link URLs; add to manifest; add README +- Auto-set script name on device from original filename in `put_script.py` +- Add default manifest path to `sync-manifest-json.py` based on script location - Remove non-production `ble/events-to-kvs.shelly.js` from manifest (missing @status) - Add remote feature branch cleanup rule to AGENTS.md git workflow - Add dev branch to CI/CD pull_request trigger diff --git a/SHELLY_MJS.md b/SHELLY_MJS.md index 079d59e..31ca5e3 100644 --- a/SHELLY_MJS.md +++ b/SHELLY_MJS.md @@ -388,6 +388,42 @@ switch-input/shelly2p-domo-coverfix.shelly.js: Shelly Plus 2PM cover fix for Dom Simple fix for outgoing Domoticz MQTTAD command 'GoToPosition'. Shelly firmware 0.x supported. Developed for ShellyTeacher4Domo. +the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js: Deye SG02LP1 MODBUS-RTU +=== +MODBUS-RTU example for reading Deye SG02LP1 solar inverter parameters over UART using the MODBUS-RTU master library. + +the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js: Deye SG02LP1 MODBUS-RTU + Virtual Components +=== +MODBUS-RTU reader for Deye SG02LP1 solar inverter with Virtual Component updates. Reads parameters over UART (RS485) and pushes values to user-defined virtual number components. + +the_pill/UART/uart_test.shelly.js: UART test +=== +Simple UART loopback test that sends periodic messages and prints received data. + +the_pill/ys_irtm/ysirtm.shelly.js: YS-IRTM infrared UART library +=== +UART protocol implementation for YS-IRTM to send and receive NEC IR codes. + +the_pill/ys_irtm/btn2ir.shelly.js: YS-IRTM button-to-IR example +=== +Maps Shelly buttons and inputs to IR codes using YS-IRTM. + +the_pill/ys_irtm/ir2sw.shelly.js: YS-IRTM IR-to-switch example +=== +Maps received IR codes to Shelly switch actions. + +the_pill/ys_irtm/ir_full.shelly.js: YS-IRTM advanced IR automation example +=== +Bidirectional IR automation with scenes, HTTP calls, and switch integration. + +the_pill/ys_irtm/ir_learn.shelly.js: YS-IRTM IR learn mode +=== +Captures and prints NEC IR codes for reuse in other scripts. + +the_pill/ys_irtm/tv_ir.shelly.js: YS-IRTM TV remote codes +=== +Preconfigured NEC IR codes for common TV brands using YS-IRTM. + weather-env/cover-control-weather.shelly.js: Control a Shelly 2.5 (Gen1) depending on current cloud conditions === The script, when run, will fetch via REST api from a weather service the current conditions for a location check if diff --git a/examples-manifest.json b/examples-manifest.json index c53c3ed..bbdd40c 100644 --- a/examples-manifest.json +++ b/examples-manifest.json @@ -438,6 +438,51 @@ "title": "Shelly Plus 2PM cover fix for Domoticz MQTTAD v1", "description": "Simple fix for outgoing Domoticz MQTTAD command 'GoToPosition'.\n Shelly firmware 0.x supported. Developed for ShellyTeacher4Domo." }, + { + "fname": "the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js", + "title": "Deye SG02LP1 MODBUS-RTU", + "description": "MODBUS-RTU example for reading Deye SG02LP1 solar inverter parameters over UART using the MODBUS-RTU master library." + }, + { + "fname": "the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js", + "title": "Deye SG02LP1 MODBUS-RTU + Virtual Components", + "description": "MODBUS-RTU reader for Deye SG02LP1 solar inverter with Virtual Component updates. Reads parameters over UART (RS485) and pushes values to user-defined virtual number components." + }, + { + "fname": "the_pill/UART/uart_test.shelly.js", + "title": "UART test", + "description": "Simple UART loopback test that sends periodic messages and prints received data." + }, + { + "fname": "the_pill/ys_irtm/ysirtm.shelly.js", + "title": "YS-IRTM infrared UART library", + "description": "UART protocol implementation for YS-IRTM to send and receive NEC IR codes." + }, + { + "fname": "the_pill/ys_irtm/btn2ir.shelly.js", + "title": "YS-IRTM button-to-IR example", + "description": "Maps Shelly buttons and inputs to IR codes using YS-IRTM." + }, + { + "fname": "the_pill/ys_irtm/ir2sw.shelly.js", + "title": "YS-IRTM IR-to-switch example", + "description": "Maps received IR codes to Shelly switch actions." + }, + { + "fname": "the_pill/ys_irtm/ir_full.shelly.js", + "title": "YS-IRTM advanced IR automation example", + "description": "Bidirectional IR automation with scenes, HTTP calls, and switch integration." + }, + { + "fname": "the_pill/ys_irtm/ir_learn.shelly.js", + "title": "YS-IRTM IR learn mode", + "description": "Captures and prints NEC IR codes for reuse in other scripts." + }, + { + "fname": "the_pill/ys_irtm/tv_ir.shelly.js", + "title": "YS-IRTM TV remote codes", + "description": "Preconfigured NEC IR codes for common TV brands using YS-IRTM." + }, { "fname": "weather-env/cover-control-weather.shelly.js", "title": "Control a Shelly 2.5 (Gen1) depending on current cloud conditions", diff --git a/the_pill/MODBUS/ComWinTop/README.md b/the_pill/MODBUS/ComWinTop/README.md new file mode 100644 index 0000000..acce189 --- /dev/null +++ b/the_pill/MODBUS/ComWinTop/README.md @@ -0,0 +1,155 @@ +# ComWinTop CWT-MB308V GPIO Expander + +> **Under Development** - This script is currently under development and may not be fully functional. + +Script for communicating with the **ComWinTop CWT-MB308V** IO module via MODBUS-RTU over RS485/UART using The Pill. + +## Files + +### [mb308v.shelly.js](mb308v.shelly.js) + +**CWT-MB308V GPIO Expander Example** - Complete example for the ComWinTop MB308V IO module. + +--- + +## Hardware Requirements + +- Shelly device with UART (e.g., **The Pill**) +- RS485 transceiver module (e.g., MAX485, SP485) +- ComWinTop CWT-MB308V IO module (7-35VDC supply) + +### Device Specifications + +| Feature | Detail | +|---|---| +| Analog Inputs (AI) | 8 channels — 4-20mA / 0-5V / 0-10V (configurable per channel) | +| Analog Outputs (AO) | 4 channels — 0-10V / 4-20mA | +| Digital Inputs (DI) | 8 channels — dry contact / NPN | +| Digital Outputs (DO) | 12 relay outputs | + +### Wiring + +**RS485 module to Shelly (The Pill):** + +| RS485 Module | Shelly / The Pill | +|---|---| +| RO (Receiver Output) | RX (GPIO) | +| DI (Driver Input) | TX (GPIO) | +| VCC | 3.3V or 5V | +| GND | GND | + +**RS485 module to CWT-MB308V:** + +| RS485 Module | CWT-MB308V | +|---|---| +| A (D+) | A (D+) | +| B (D-) | B (D-) | + +**Power:** Connect 7-35VDC to the MB308V power terminals separately. + +**UART Settings:** 9600 baud, 8N1, Slave ID: 1 (default) + +--- + +## Register Map + +| Type | Function Code | Address Range | Count | +|---|---|---|---| +| Digital Outputs (DO) | FC 0x01 (read) / FC 0x05 (write) | 0–11 | 12 coils | +| Digital Inputs (DI) | FC 0x02 (read) | 0–7 | 8 inputs | +| Analog Outputs (AO) | FC 0x03 (read) / FC 0x06 (write) | 0–3 | 4 registers | +| Analog Inputs (AI) | FC 0x04 (read) | 0–7 | 8 registers | + +--- + +## API Methods + +```javascript +readDigitalInputs(callback) // Read 8 DI +readDigitalOutputs(callback) // Read 12 DO (relays) +writeDigitalOutput(channel, value, cb) // Set relay (0-11, true/false) +readAnalogInputs(callback) // Read 8 AI +readAnalogOutputs(callback) // Read 4 AO +writeAnalogOutput(channel, value, cb) // Set AO (0-3, 0-24000) + +// Conversion helpers +aiToMilliamps(raw) // Convert AI to mA (4-20mA mode) +aiToVoltage(raw) // Convert AI to V (0-10V mode) +milliampsToAo(mA) // Convert mA to AO value +voltageToAo(volts) // Convert V to AO value +``` + +--- + +## Usage Examples + +**Read all digital inputs:** + +```javascript +readDigitalInputs(function(err, inputs) { + if (err) { + print("Error: " + err); + return; + } + for (var i = 0; i < inputs.length; i++) { + print("DI" + i + ": " + (inputs[i] ? "ON" : "OFF")); + } +}); +``` + +**Control relay output:** + +```javascript +// Turn ON relay 0 +writeDigitalOutput(0, true, function(err, success) { + if (success) print("Relay 0 ON"); +}); + +// Turn OFF relay 5 +writeDigitalOutput(5, false, function(err, success) { + if (success) print("Relay 5 OFF"); +}); +``` + +**Read analog inputs (4-20mA sensors):** + +```javascript +readAnalogInputs(function(err, values) { + if (err) return; + for (var i = 0; i < values.length; i++) { + var mA = aiToMilliamps(values[i]); + print("AI" + i + ": " + mA.toFixed(2) + " mA"); + } +}); +``` + +**Set analog output (0-10V mode):** + +```javascript +// Set AO0 to 5V +var rawValue = voltageToAo(5.0); +writeAnalogOutput(0, rawValue, function(err, success) { + if (success) print("AO0 set to 5V"); +}); +``` + +--- + +## Configuration + +```javascript +var CONFIG = { + BAUD_RATE: 9600, // 9600, 19200, 38400, 115200 + MODE: "8N1", // "8N1", "8E1", "8O1" + RESPONSE_TIMEOUT: 1000, // ms + DEBUG: true +}; +``` + +--- + +## References + +- [ComWinTop CWT-MB308V Product Page](https://store.comwintop.com/products/cwt-mb308v-8ai-4ao-8di-12do-rs485-rs232-ethernet-modbus-rtu-tcp-io-acquisition-module) +- [MB308V Python Driver (reference)](https://github.com/bgerp/ztm/blob/master/Zontromat/devices/vendors/cwt/mb308v/mb308v.py) +- [MODBUS Protocol Specification](https://modbus.org/specs.php) diff --git a/the_pill/MODBUS/mb308v.shelly.js b/the_pill/MODBUS/ComWinTop/mb308v.shelly.js similarity index 100% rename from the_pill/MODBUS/mb308v.shelly.js rename to the_pill/MODBUS/ComWinTop/mb308v.shelly.js diff --git a/the_pill/MODBUS/Deye/README.md b/the_pill/MODBUS/Deye/README.md new file mode 100644 index 0000000..54ee690 --- /dev/null +++ b/the_pill/MODBUS/Deye/README.md @@ -0,0 +1,148 @@ +# Deye SG02LP1 Solar Inverter - MODBUS-RTU Reader + +> **Under Development** - These scripts are currently under development and may not be fully functional. + +Scripts for reading live data from a **Deye SG02LP1 hybrid solar inverter** over MODBUS-RTU via RS485/UART using The Pill. + +## Files + +### [the_pill_mbsa_deye.shelly.js](the_pill_mbsa_deye.shelly.js) + +**Basic reader** - Polls the inverter every 10 seconds and prints all parameter values to the Shelly script console. + +### [the_pill_mbsa_deye_vc.shelly.js](the_pill_mbsa_deye_vc.shelly.js) + +**Reader + Virtual Components** - Same polling logic, but also pushes each value into a Shelly Virtual Number component so values are accessible via the Shelly RPC API and the app. + +--- + +## Hardware Requirements + +- Shelly device with UART (e.g., **The Pill**) +- RS485 transceiver module (e.g., MAX485, SP485) +- Deye SG02LP1 hybrid inverter with RS485 port + +### Wiring + +**RS485 module to Shelly (The Pill):** + +| RS485 Module | Shelly / The Pill | +|---|---| +| RO (Receiver Output) | RX (GPIO) | +| DI (Driver Input) | TX (GPIO) | +| VCC | 3.3V or 5V | +| GND | GND | + +**RS485 module to Deye inverter:** + +| RS485 Module | Deye RS485 Port | +|---|---| +| A (D+) | A (D+) | +| B (D-) | B (D-) | +| GND | GND (optional, for noise rejection) | + +**UART Settings:** 9600 baud, 8N1 + +> The Deye RS485 port is typically a 3-pin or RJ45 connector on the communication board. Consult your inverter manual for the exact pinout. + +--- + +## Monitored Parameters + +Both scripts read the same 9 registers from the inverter: + +| Parameter | Register | Type | Scale | Units | +|---|---|---|---|---| +| Total Power | 175 | i16 | 1 | W | +| Battery Power | 190 | i16 | 1 | W | +| PV1 Power | 186 | u16 | 1 | W | +| Total Grid Power | 169 | i16 | 10 | W | +| Battery SOC | 184 | u16 | 1 | % | +| PV1 Voltage | 109 | u16 | 0.1 | V | +| Grid Voltage L1 | 150 | u16 | 0.1 | V | +| Current L1 | 164 | i16 | 0.01 | A | +| AC Frequency | 192 | u16 | 0.01 | Hz | + +`i16` registers are treated as signed 16-bit integers; `u16` as unsigned. The raw register value is multiplied by `scale` to get the physical value. + +--- + +## Virtual Component Mapping (\_vc variant only) + +The `_vc` script maps each parameter to a pre-existing Shelly Virtual Number component. You must create these components on the device before running the script (via the Shelly app or RPC). + +| Parameter | Virtual Component ID | +|---|---| +| Total Power | `number:200` | +| Battery Power | `number:201` | +| PV1 Power | `number:202` | +| Total Grid Power | `number:203` | +| Battery SOC | `number:204` | +| PV1 Voltage | `number:205` | +| Grid Voltage L1 | `number:206` | +| Current L1 | `number:207` | +| AC Frequency | `number:208` | + +Values are updated every poll cycle. If a component handle cannot be obtained (component not created), that parameter is polled but not pushed. + +--- + +## Configuration + +Both scripts share the same `CONFIG` block at the top of the file: + +```javascript +var CONFIG = { + BAUD_RATE: 9600, // UART baud rate (must match inverter setting) + MODE: "8N1", // UART framing + SLAVE_ID: 1, // MODBUS slave address of the inverter + RESPONSE_TIMEOUT: 1000, // ms to wait for a register response + POLL_INTERVAL: 10000, // ms between full poll cycles (10 s) + DEBUG: true // print TX/RX frames and value changes to console +}; +``` + +> If you have multiple devices on the RS485 bus, change `SLAVE_ID` to match your inverter's configured address. + +--- + +## Console Output Example + +``` +Deye SG02LP1 - MODBUS-RTU Reader +================================= +[DEYE] UART: 9600 baud, 8N1 +[DEYE] Slave ID: 1 + +Polling 9 parameters every 10s... + +--- Deye SG02LP1 --- +Total Power: 1450 [W] +Battery Power: -320 [W] +PV1 Power: 2100 [W] +Total Grid Power: 0 [W] +Battery SOC: 87 [%] +PV1 Voltage: 342.5 [V] +Grid Voltage L1: 231.2 [V] +Current L1: 6.28 [A] +AC Frequency: 50.01 [Hz] +``` + +--- + +## Implementation Notes + +- Registers are read **one at a time** (FC 0x03, quantity = 1) with a 50 ms inter-request delay for bus stability. +- CRC-16 is computed via a pre-computed lookup table (MODBUS polynomial 0xA001). +- The MODBUS exception response (function code | 0x80) is detected and surfaced as an error string. +- A 1-second response timeout is enforced per request; timed-out parameters are logged as `ERROR (Timeout)`. +- There is no write capability - these scripts are read-only. + +--- + +## References + +- [Deye Inverter Product Page](https://www.deyeinverter.com/) +- [MODBUS RTU Protocol Specification](https://modbus.org/specs.php) +- [MODBUS over Serial Line](https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf) +- [Shelly Virtual Components](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Virtual) diff --git a/the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js b/the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js new file mode 100644 index 0000000..88aa3fa --- /dev/null +++ b/the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js @@ -0,0 +1,372 @@ +/** + * @title Deye SG02LP1 MODBUS-RTU + * @description MODBUS-RTU example for reading Deye SG02LP1 solar inverter + * parameters over UART using the MODBUS-RTU master library. + * @status production + * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/MODBUS/Deye/the_pill_mbsa_deye.shelly.js + */ + +/** + * Deye SG02LP1 Solar Inverter - MODBUS-RTU Reader + * + * Reads key parameters from a Deye SG02LP1 hybrid inverter via + * MODBUS-RTU over UART (RS485). + * + * Monitored parameters: + * - Total Power, Battery Power, PV1 Power + * - Total Grid Power, Battery SOC + * - PV1 Voltage, Grid Voltage L1, Current L1 + * - AC Frequency + * + * Hardware connection: + * - RS485 Module TX -> Shelly RX (GPIO) + * - RS485 Module RX -> Shelly TX (GPIO) + * - RS485 Module A/B -> Inverter RS485 A/B + * - GND -> GND + */ + +/* === CONFIG === */ +var CONFIG = { + BAUD_RATE: 9600, + MODE: "8N1", + SLAVE_ID: 1, + RESPONSE_TIMEOUT: 1000, + POLL_INTERVAL: 10000, + DEBUG: true +}; + +/* === DEYE REGISTER MAP === */ +var ENTITIES = [ + { name: "Total Power", units: "W", addr: 175, itype: "i16", scale: 1 }, + { name: "Battery Power", units: "W", addr: 190, itype: "i16", scale: 1 }, + { name: "PV1 Power", units: "W", addr: 186, itype: "u16", scale: 1 }, + { name: "Total Grid Power",units: "W", addr: 169, itype: "i16", scale: 10 }, + { name: "Battery SOC", units: "%", addr: 184, itype: "u16", scale: 1 }, + { name: "PV1 Voltage", units: "V", addr: 109, itype: "u16", scale: 0.1 }, + { name: "Grid Voltage L1", units: "V", addr: 150, itype: "u16", scale: 0.1 }, + { name: "Current L1", units: "A", addr: 164, itype: "i16", scale: 0.01 }, + { name: "AC Frequency", units: "Hz", addr: 192, itype: "u16", scale: 0.01 } +]; + +/* === MODBUS FUNCTION CODES === */ +var FC = { + READ_HOLDING_REGISTERS: 0x03 +}; + +/* === CRC-16 TABLE (MODBUS polynomial 0xA001) === */ +var CRC_TABLE = [ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +]; + +/* === STATE === */ +var state = { + uart: null, + rxBuffer: [], + isReady: false, + pendingRequest: null, + responseTimer: null, + pollTimer: null +}; + +/* === HELPERS === */ + +function toHex(n) { + n = n & 0xFF; + return (n < 16 ? "0" : "") + n.toString(16).toUpperCase(); +} + +function bytesToHex(bytes) { + var hex = ""; + for (var i = 0; i < bytes.length; i++) { + hex += toHex(bytes[i]); + if (i < bytes.length - 1) hex += " "; + } + return hex; +} + +function debug(msg) { + if (CONFIG.DEBUG) { + print("[DEYE] " + msg); + } +} + +function calcCRC(bytes) { + var crc = 0xFFFF; + for (var i = 0; i < bytes.length; i++) { + var index = (crc ^ bytes[i]) & 0xFF; + crc = (crc >> 8) ^ CRC_TABLE[index]; + } + return crc; +} + +function bytesToStr(bytes) { + var s = ""; + for (var i = 0; i < bytes.length; i++) { + s += String.fromCharCode(bytes[i] & 0xFF); + } + return s; +} + +function buildFrame(slaveAddr, functionCode, data) { + var frame = [slaveAddr & 0xFF, functionCode & 0xFF]; + if (data) { + for (var i = 0; i < data.length; i++) { + frame.push(data[i] & 0xFF); + } + } + var crc = calcCRC(frame); + frame.push(crc & 0xFF); + frame.push((crc >> 8) & 0xFF); + return frame; +} + +/* === MODBUS CORE === */ + +function sendRequest(functionCode, data, callback) { + if (!state.isReady) { + callback("Not initialized", null); + return; + } + if (state.pendingRequest) { + callback("Request pending", null); + return; + } + + var frame = buildFrame(CONFIG.SLAVE_ID, functionCode, data); + debug("TX: " + bytesToHex(frame)); + + state.pendingRequest = { + functionCode: functionCode, + callback: callback + }; + state.rxBuffer = []; + + state.responseTimer = Timer.set(CONFIG.RESPONSE_TIMEOUT, false, function() { + if (state.pendingRequest) { + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + debug("Timeout"); + cb("Timeout", null); + } + }); + + state.uart.write(bytesToStr(frame)); +} + +function onReceive(data) { + if (!data || data.length === 0) return; + + for (var i = 0; i < data.length; i++) { + state.rxBuffer.push(data.charCodeAt(i) & 0xFF); + } + + processResponse(); +} + +function processResponse() { + if (!state.pendingRequest) { + state.rxBuffer = []; + return; + } + + if (state.rxBuffer.length < 5) return; + + var fc = state.rxBuffer[1]; + + // Check exception + if (fc & 0x80) { + if (state.rxBuffer.length >= 5) { + var excFrame = state.rxBuffer.slice(0, 5); + var crc = calcCRC(excFrame.slice(0, 3)); + var recvCrc = excFrame[3] | (excFrame[4] << 8); + if (crc === recvCrc) { + clearResponseTimeout(); + var exCode = state.rxBuffer[2]; + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb("Exception: 0x" + toHex(exCode), null); + } + } + return; + } + + // For read holding registers: slave(1) + FC(1) + byteCount(1) + data(N) + CRC(2) + var expectedLen = 0; + if (fc === FC.READ_HOLDING_REGISTERS && state.rxBuffer.length >= 3) { + expectedLen = 3 + state.rxBuffer[2] + 2; + } + + if (expectedLen === 0 || state.rxBuffer.length < expectedLen) return; + + var frame = state.rxBuffer.slice(0, expectedLen); + var crc = calcCRC(frame.slice(0, expectedLen - 2)); + var recvCrc = frame[expectedLen - 2] | (frame[expectedLen - 1] << 8); + + if (crc !== recvCrc) { + debug("CRC error"); + return; + } + + debug("RX: " + bytesToHex(frame)); + clearResponseTimeout(); + + var responseData = frame.slice(2, expectedLen - 2); + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb(null, responseData); +} + +function clearResponseTimeout() { + if (state.responseTimer) { + Timer.clear(state.responseTimer); + state.responseTimer = null; + } +} + +/* === DEYE API === */ + +/** + * Read a single holding register + * @param {number} addr - Register address + * @param {function} callback - callback(error, rawValue) + */ +function readRegister(addr, callback) { + var data = [ + (addr >> 8) & 0xFF, + addr & 0xFF, + 0x00, + 0x01 + ]; + + sendRequest(FC.READ_HOLDING_REGISTERS, data, function(err, response) { + if (err) { + callback(err, null); + return; + } + // response: [byteCount, highByte, lowByte] + if (response.length < 3) { + callback("Short response", null); + return; + } + var raw = (response[1] << 8) | response[2]; + callback(null, raw); + }); +} + +/** + * Interpret raw u16 value as signed i16 + */ +function toSigned16(val) { + if (val >= 0x8000) { + return val - 0x10000; + } + return val; +} + +/** + * Read all entities sequentially, then print results + */ +function pollEntities() { + var results = []; + + function readNext(index) { + if (index >= ENTITIES.length) { + // All done, print results + print("--- Deye SG02LP1 ---"); + for (var i = 0; i < results.length; i++) { + print(results[i]); + } + print(""); + return; + } + + var entity = ENTITIES[index]; + readRegister(entity.addr, function(err, raw) { + if (err) { + results.push(entity.name + ": ERROR (" + err + ")"); + } else { + var value = entity.itype === "i16" ? toSigned16(raw) : raw; + value = value * entity.scale; + results.push(entity.name + ": " + value + " [" + entity.units + "]"); + } + // Small delay between requests for bus stability + Timer.set(50, false, function() { + readNext(index + 1); + }); + }); + } + + readNext(0); +} + +/* === INITIALIZATION === */ + +function init() { + print("Deye SG02LP1 - MODBUS-RTU Reader"); + print("================================="); + + state.uart = UART.get(); + if (!state.uart) { + print("ERROR: UART not available"); + return; + } + + if (!state.uart.configure({ + baud: CONFIG.BAUD_RATE, + mode: CONFIG.MODE + })) { + print("ERROR: UART configuration failed"); + return; + } + + state.uart.recv(onReceive); + state.isReady = true; + + debug("UART: " + CONFIG.BAUD_RATE + " baud, " + CONFIG.MODE); + debug("Slave ID: " + CONFIG.SLAVE_ID); + print(""); + + print("Polling " + ENTITIES.length + " parameters every " + (CONFIG.POLL_INTERVAL / 1000) + "s..."); + print(""); + + // Initial poll + Timer.set(500, false, pollEntities); + + // Periodic polling + state.pollTimer = Timer.set(CONFIG.POLL_INTERVAL, true, pollEntities); +} + +init(); diff --git a/the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js b/the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js new file mode 100644 index 0000000..6c1d9a8 --- /dev/null +++ b/the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js @@ -0,0 +1,357 @@ +/** + * @title Deye SG02LP1 MODBUS-RTU + Virtual Components + * @description MODBUS-RTU reader for Deye SG02LP1 solar inverter with + * Virtual Component updates. Reads parameters over UART (RS485) and + * pushes values to user-defined virtual number components. + * @status production + * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/MODBUS/Deye/the_pill_mbsa_deye_vc.shelly.js + */ + +/* === CONFIG === */ +var CONFIG = { + BAUD_RATE: 9600, + MODE: "8N1", + SLAVE_ID: 1, + RESPONSE_TIMEOUT: 1000, + POLL_INTERVAL: 10000, + DEBUG: true +}; + +/* === DEYE REGISTER MAP + VIRTUAL COMPONENT MAPPING === */ +var ENTITIES = [ + { name: "Total Power", units: "W", addr: 175, itype: "i16", scale: 1, vcId: "number:200", vcHandle: null }, + { name: "Battery Power", units: "W", addr: 190, itype: "i16", scale: 1, vcId: "number:201", vcHandle: null }, + { name: "PV1 Power", units: "W", addr: 186, itype: "u16", scale: 1, vcId: "number:202", vcHandle: null }, + { name: "Total Grid Power",units: "W", addr: 169, itype: "i16", scale: 10, vcId: "number:203", vcHandle: null }, + { name: "Battery SOC", units: "%", addr: 184, itype: "u16", scale: 1, vcId: "number:204", vcHandle: null }, + { name: "PV1 Voltage", units: "V", addr: 109, itype: "u16", scale: 0.1, vcId: "number:205", vcHandle: null }, + { name: "Grid Voltage L1", units: "V", addr: 150, itype: "u16", scale: 0.1, vcId: "number:206", vcHandle: null }, + { name: "Current L1", units: "A", addr: 164, itype: "i16", scale: 0.01, vcId: "number:207", vcHandle: null }, + { name: "AC Frequency", units: "Hz", addr: 192, itype: "u16", scale: 0.01, vcId: "number:208", vcHandle: null } +]; + +/* === MODBUS FUNCTION CODES === */ +var FC = { + READ_HOLDING_REGISTERS: 0x03 +}; + +/* === CRC-16 TABLE (MODBUS polynomial 0xA001) === */ +var CRC_TABLE = [ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +]; + +/* === STATE === */ +var state = { + uart: null, + rxBuffer: [], + isReady: false, + pendingRequest: null, + responseTimer: null, + pollTimer: null +}; + +/* === HELPERS === */ + +function toHex(n) { + n = n & 0xFF; + return (n < 16 ? "0" : "") + n.toString(16).toUpperCase(); +} + +function bytesToHex(bytes) { + var hex = ""; + for (var i = 0; i < bytes.length; i++) { + hex += toHex(bytes[i]); + if (i < bytes.length - 1) hex += " "; + } + return hex; +} + +function debug(msg) { + if (CONFIG.DEBUG) { + print("[DEYE] " + msg); + } +} + +function calcCRC(bytes) { + var crc = 0xFFFF; + for (var i = 0; i < bytes.length; i++) { + var index = (crc ^ bytes[i]) & 0xFF; + crc = (crc >> 8) ^ CRC_TABLE[index]; + } + return crc; +} + +function bytesToStr(bytes) { + var s = ""; + for (var i = 0; i < bytes.length; i++) { + s += String.fromCharCode(bytes[i] & 0xFF); + } + return s; +} + +function buildFrame(slaveAddr, functionCode, data) { + var frame = [slaveAddr & 0xFF, functionCode & 0xFF]; + if (data) { + for (var i = 0; i < data.length; i++) { + frame.push(data[i] & 0xFF); + } + } + var crc = calcCRC(frame); + frame.push(crc & 0xFF); + frame.push((crc >> 8) & 0xFF); + return frame; +} + +/* === MODBUS CORE === */ + +function sendRequest(functionCode, data, callback) { + if (!state.isReady) { + callback("Not initialized", null); + return; + } + if (state.pendingRequest) { + callback("Request pending", null); + return; + } + + var frame = buildFrame(CONFIG.SLAVE_ID, functionCode, data); + debug("TX: " + bytesToHex(frame)); + + state.pendingRequest = { + functionCode: functionCode, + callback: callback + }; + state.rxBuffer = []; + + state.responseTimer = Timer.set(CONFIG.RESPONSE_TIMEOUT, false, function() { + if (state.pendingRequest) { + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + debug("Timeout"); + cb("Timeout", null); + } + }); + + state.uart.write(bytesToStr(frame)); +} + +function onReceive(data) { + if (!data || data.length === 0) return; + + for (var i = 0; i < data.length; i++) { + state.rxBuffer.push(data.charCodeAt(i) & 0xFF); + } + + processResponse(); +} + +function processResponse() { + if (!state.pendingRequest) { + state.rxBuffer = []; + return; + } + + if (state.rxBuffer.length < 5) return; + + var fc = state.rxBuffer[1]; + + // Check exception + if (fc & 0x80) { + if (state.rxBuffer.length >= 5) { + var excFrame = state.rxBuffer.slice(0, 5); + var crc = calcCRC(excFrame.slice(0, 3)); + var recvCrc = excFrame[3] | (excFrame[4] << 8); + if (crc === recvCrc) { + clearResponseTimeout(); + var exCode = state.rxBuffer[2]; + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb("Exception: 0x" + toHex(exCode), null); + } + } + return; + } + + // For read holding registers: slave(1) + FC(1) + byteCount(1) + data(N) + CRC(2) + var expectedLen = 0; + if (fc === FC.READ_HOLDING_REGISTERS && state.rxBuffer.length >= 3) { + expectedLen = 3 + state.rxBuffer[2] + 2; + } + + if (expectedLen === 0 || state.rxBuffer.length < expectedLen) return; + + var frame = state.rxBuffer.slice(0, expectedLen); + var crc = calcCRC(frame.slice(0, expectedLen - 2)); + var recvCrc = frame[expectedLen - 2] | (frame[expectedLen - 1] << 8); + + if (crc !== recvCrc) { + debug("CRC error"); + return; + } + + debug("RX: " + bytesToHex(frame)); + clearResponseTimeout(); + + var responseData = frame.slice(2, expectedLen - 2); + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb(null, responseData); +} + +function clearResponseTimeout() { + if (state.responseTimer) { + Timer.clear(state.responseTimer); + state.responseTimer = null; + } +} + +/* === DEYE API === */ + +function readRegister(addr, callback) { + var data = [ + (addr >> 8) & 0xFF, + addr & 0xFF, + 0x00, + 0x01 + ]; + + sendRequest(FC.READ_HOLDING_REGISTERS, data, function(err, response) { + if (err) { + callback(err, null); + return; + } + if (response.length < 3) { + callback("Short response", null); + return; + } + var raw = (response[1] << 8) | response[2]; + callback(null, raw); + }); +} + +function toSigned16(val) { + if (val >= 0x8000) { + return val - 0x10000; + } + return val; +} + +function updateVc(entity, value) { + if (!entity.vcHandle) return; + var oldVal = entity.vcHandle.getValue(); + entity.vcHandle.setValue(value); + debug(entity.name + ": " + oldVal + " -> " + value + " [" + entity.units + "]"); +} + +function pollEntities() { + var results = []; + + function readNext(index) { + if (index >= ENTITIES.length) { + print("--- Deye SG02LP1 ---"); + for (var i = 0; i < results.length; i++) { + print(results[i]); + } + print(""); + return; + } + + var entity = ENTITIES[index]; + readRegister(entity.addr, function(err, raw) { + if (err) { + results.push(entity.name + ": ERROR (" + err + ")"); + } else { + var value = entity.itype === "i16" ? toSigned16(raw) : raw; + value = value * entity.scale; + results.push(entity.name + ": " + value + " [" + entity.units + "]"); + updateVc(entity, value); + } + Timer.set(50, false, function() { + readNext(index + 1); + }); + }); + } + + readNext(0); +} + +/* === INITIALIZATION === */ + +function init() { + print("Deye SG02LP1 - MODBUS-RTU Reader + Virtual Components"); + print("======================================================"); + + // Initialize virtual component handles + for (var i = 0; i < ENTITIES.length; i++) { + var ent = ENTITIES[i]; + if (ent.vcId) { + ent.vcHandle = Virtual.getHandle(ent.vcId); + debug("VC handle for " + ent.name + " -> " + ent.vcId); + } + } + + state.uart = UART.get(); + if (!state.uart) { + print("ERROR: UART not available"); + return; + } + + if (!state.uart.configure({ + baud: CONFIG.BAUD_RATE, + mode: CONFIG.MODE + })) { + print("ERROR: UART configuration failed"); + return; + } + + state.uart.recv(onReceive); + state.isReady = true; + + debug("UART: " + CONFIG.BAUD_RATE + " baud, " + CONFIG.MODE); + debug("Slave ID: " + CONFIG.SLAVE_ID); + print(""); + + print("Polling " + ENTITIES.length + " parameters every " + (CONFIG.POLL_INTERVAL / 1000) + "s..."); + print(""); + + // Initial poll + Timer.set(500, false, pollEntities); + + // Periodic polling + state.pollTimer = Timer.set(CONFIG.POLL_INTERVAL, true, pollEntities); +} + +init(); diff --git a/the_pill/MODBUS/JK200-MBS/README.md b/the_pill/MODBUS/JK200-MBS/README.md new file mode 100644 index 0000000..bbcbce5 --- /dev/null +++ b/the_pill/MODBUS/JK200-MBS/README.md @@ -0,0 +1,185 @@ +# JK200 BMS - MODBUS-RTU Reader + +> **Under Development** - This script is currently under development and may not be fully functional. + +Script for reading live data from a **Jikong JK-PB series BMS** (commonly called JK200 for the 200A variants) over MODBUS-RTU via RS485/UART using The Pill. + +Compatible models: JK-PB2A8S20P, JK-PB2A16S20P, JK-PB2A20S20P (and other PB-series variants). + +## Files + +### [the_pill_mbsa_jk200.shelly.js](the_pill_mbsa_jk200.shelly.js) + +Reads two register blocks per poll cycle and prints a full status report to the Shelly script console: + +- All individual cell voltages with min/max/delta +- Pack voltage, current, power +- State of Charge (SOC) +- MOSFET temperature, battery temperature sensors 1 & 2 +- Balance current +- Active alarm flags + +--- + +## Enable MODBUS on the BMS + +By default the JK BMS communicates over its own proprietary protocol. To activate RS485 MODBUS slave mode: + +1. Open the **JiKong BMS** app and connect via Bluetooth. +2. Go to **Settings → Device Address**. +3. Set the address to any value from **1 to 15** (0 = disabled). +4. The chosen address becomes the MODBUS slave ID. + +Default communication: **9600 baud, 8N1**. + +--- + +## Hardware Requirements + +- Shelly device with UART (e.g., **The Pill**) +- RS485 transceiver module (e.g., MAX485, SP485) +- Jikong JK-PB series BMS with RS485 connector + +### Wiring + +**RS485 module to Shelly (The Pill):** + +| RS485 Module | Shelly / The Pill | +|---|---| +| RO (Receiver Output) | RX (GPIO) | +| DI (Driver Input) | TX (GPIO) | +| VCC | 3.3V or 5V | +| GND | GND | + +**RS485 module to JK BMS:** + +| RS485 Module | JK BMS RS485 Port | +|---|---| +| A (D+) | A (D+) | +| B (D-) | B (D-) | + +> The JK BMS RS485 port is a 4-pin JST-style connector. Typical pinout: GND, A, B, +5V. Consult your BMS manual for the exact connector layout — not all units are identical. + +--- + +## Register Map + +The JK BMS uses **stride-2 MODBUS addressing** (JK BMS RS485 Modbus V1.0 spec): + +| Value width | MODBUS registers used | Layout | +|---|---|---| +| U_WORD / S_WORD (16-bit) | 2 | `[data, padding]` | +| U_DWORD / S_DWORD (32-bit) | 4 | `[hi, lo, padding, padding]` | + +### Block A — Cell Voltages (`FC 0x03`, start `0x1200`) + +| Address | Parameter | Type | Unit | +|---|---|---|---| +| 0x1200 | Cell 1 voltage | U_WORD | mV | +| 0x1202 | Cell 2 voltage | U_WORD | mV | +| … | … | … | … | +| 0x1200 + (N-1)×2 | Cell N voltage | U_WORD | mV | + +Read quantity = `CELL_COUNT × 2` registers. Cell N voltage = `registers[(N-1) × 2]`. + +### Block B — Key Parameters (`FC 0x03`, start `0x128A`, qty 30) + +| Address | Offset in response | Parameter | Type | Unit | Notes | +|---|---|---|---|---|---| +| 0x128A | regs[0] | MOSFET temperature | S_WORD | 0.1 °C | | +| 0x128B | regs[1] | (padding) | — | — | | +| 0x128C–0x128F | regs[2–5] | (reserved) | — | — | | +| 0x1290 | regs[6–7] | Pack voltage | U_DWORD | mV | `regs[6]×65536 + regs[7]` | +| 0x1292 | regs[8–9] | (padding) | — | — | | +| 0x1294 | regs[10–11] | Pack power | S_DWORD | mW | + = charging | +| 0x1296 | regs[12–13] | (padding) | — | — | | +| 0x1298 | regs[14–15] | Pack current | S_DWORD | mA | + = charging | +| 0x129A | regs[16–17] | (padding) | — | — | | +| 0x129C | regs[18] | Temperature 1 | S_WORD | 0.1 °C | | +| 0x129E | regs[20] | Temperature 2 | S_WORD | 0.1 °C | | +| 0x12A0 | regs[22–23] | Alarm bitmask | U_DWORD | — | see below | +| 0x12A4 | regs[26] | Balance current | S_WORD | mA | | +| 0x12A6 | regs[28] | State of Charge | U_WORD | % | | + +### Alarm Bitmask + +| Bit | Meaning | +|---|---| +| 0 | Cell undervoltage | +| 1 | Cell overvoltage | +| 2 | Discharge overcurrent | +| 3 | Charge overcurrent | +| 4 | Low temperature (charge) | +| 5 | High temperature (discharge) | +| 6 | MOS overtemperature | +| 7 | Short circuit | +| 8 | Cell delta too large | +| 9 | Pack undervoltage | +| 10 | Pack overvoltage | +| 11 | Low SOC | +| 15 | Manual shutdown | + +--- + +## Configuration + +```javascript +var CONFIG = { + BAUD_RATE: 9600, // must match BMS setting + MODE: '8N1', + SLAVE_ID: 1, // must match BMS Device Address setting + CELL_COUNT: 16, // 8 / 10 / 12 / 14 / 16 / 20 / 24 + RESPONSE_TIMEOUT: 2000, // ms — generous for bulk reads at 9600 baud + INTER_READ_DELAY: 100, // ms between block A and block B reads + POLL_INTERVAL: 10000, // ms between full poll cycles + DEBUG: false, // true = print raw TX/RX frames +}; +``` + +> Set `CELL_COUNT` to match your battery pack. Common values: 8 (24 V), 16 (48 V), 20 (60 V), 24 (72 V). + +--- + +## Console Output Example + +``` +JK200 BMS - MODBUS-RTU Reader +============================== +Cells: 16 | Poll: 10 s + +--- JK200 BMS --- + Cells (16): + 1: 3.412 V + 2: 3.411 V + 3: 3.413 V (max) + 4: 3.410 V (min) + ... + 16: 3.412 V + Delta: 0.003 V | Min: 3.410 V (cell 4) | Max: 3.413 V (cell 3) + Pack: 54.592 V | 48.500 A | 2647.712 W + SOC: 78 % + Temp: MOS 34.5 C | T1 27.8 C | T2 28.1 C + Balance: 0.050 A + Alarms: none +``` + +--- + +## Implementation Notes + +- Only FC 0x03 (Read Holding Registers) is used — the script is **read-only**. +- Two bulk reads per poll: block A (cell voltages) then block B (parameters), with a 100 ms inter-read delay for bus stability. +- CRC-16 is computed via lookup table (MODBUS polynomial 0xA001). +- MODBUS exception responses are detected (FC | 0x80) and surfaced as error strings. +- A configurable response timeout (default 2 s) guards each request. +- Signed 32-bit values (power, current) are assembled from two 16-bit registers using integer arithmetic, avoiding bitshift overflow in mJS. + +--- + +## References + +- [JK BMS RS485 Modbus V1.0 Protocol](https://github.com/ciciban/jkbms-PB2A16S20P) +- [ESPHome JK-BMS integration (syssi)](https://github.com/syssi/esphome-jk-bms) +- [ESPHome JK-BMS MODBUS YAML example](https://github.com/syssi/esphome-jk-bms/blob/main/esp32-jk-pb-modbus-example.yaml) +- [JK BMS RS485 Modbus V1.1 PDF](https://github.com/syssi/esphome-jk-bms/blob/main/docs/pb2a16s20p/BMS%20RS485%20Modbus%20V1.1.pdf) +- [MODBUS Protocol Specification](https://modbus.org/specs.php) diff --git a/the_pill/MODBUS/JK200-MBS/the_pill_mbsa_jk200.shelly.js b/the_pill/MODBUS/JK200-MBS/the_pill_mbsa_jk200.shelly.js new file mode 100644 index 0000000..042c9e0 --- /dev/null +++ b/the_pill/MODBUS/JK200-MBS/the_pill_mbsa_jk200.shelly.js @@ -0,0 +1,537 @@ +/** + * @title JK200 BMS MODBUS-RTU Reader + * @description MODBUS-RTU reader for Jikong JK-PB series BMS over RS485. + * Reads cell voltages, pack voltage, current, SOC, temperatures and alarms. + * @status under development + * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/MODBUS/JK200-MBS/the_pill_mbsa_jk200.shelly.js + */ + +/** + * JK200 BMS - MODBUS-RTU Reader for Shelly (The Pill) + * + * Compatible with Jikong JK-PB series BMS: + * JK-PB2A8S20P, JK-PB2A16S20P, JK-PB2A20S20P (and other PB variants). + * + * To enable MODBUS on the BMS: + * Open the JK BMS app → Settings → Device Address → set to 1-15. + * Any non-zero address activates RS485 Modbus slave mode. + * Default: 9600 baud, 8N1. + * + * Hardware Connection (via RS485 transceiver, e.g. MAX485): + * RS485 A (D+) <-> BMS RS485 A (D+) + * RS485 B (D-) <-> BMS RS485 B (D-) + * RS485 RO -> Shelly RX (GPIO) + * RS485 DI -> Shelly TX (GPIO) + * RS485 VCC -> 3.3V or 5V + * RS485 GND -> GND + * + * Addressing scheme (JK BMS RS485 Modbus V1.0): + * - Supports only FC 0x03 (Read Holding Registers). + * - U_WORD (16-bit): stride 2 — 1 register data + 1 register padding. + * - U_DWORD (32-bit): stride 4 — 2 registers data (hi,lo) + 2 registers padding. + * - S_WORD / S_DWORD: same strides, interpreted as signed (two's complement). + * + * Two register blocks are read per poll cycle: + * Block A — Cell voltages: FC 0x03, start 0x1200, qty CELL_COUNT * 2 + * Block B — Key parameters: FC 0x03, start 0x128A, qty 30 + * + * Block B register layout (qty 30, start 0x128A): + * Offset Reg addr Field Type Unit + * 0 0x128A MOSFET temp S_WORD 0.1 °C + * 1 0x128B (padding) + * 2-5 0x128C-F (reserved) + * 6-9 0x1290 Pack voltage U_DWORD mV + * 10-13 0x1294 Pack power S_DWORD mW + * 14-17 0x1298 Pack current S_DWORD mA + * 18-19 0x129C Temperature 1 S_WORD 0.1 °C + * 20-21 0x129E Temperature 2 S_WORD 0.1 °C + * 22-25 0x12A0 Alarm bitmask U_DWORD — + * 26-27 0x12A4 Balance current S_WORD mA + * 28-29 0x12A6 State of Charge U_WORD % + * + * References: + * JK BMS RS485 Modbus V1.0: https://github.com/ciciban/jkbms-PB2A16S20P + * ESPHome integration: https://github.com/syssi/esphome-jk-bms + */ + +/* === CONFIG === */ +var CONFIG = { + BAUD_RATE: 9600, + MODE: '8N1', + SLAVE_ID: 1, + CELL_COUNT: 16, // 8, 10, 12, 14, 16, 20, 24 — match your pack + RESPONSE_TIMEOUT: 2000, // ms; larger for bulk reads at 9600 baud + INTER_READ_DELAY: 100, // ms between block A and block B reads + POLL_INTERVAL: 10000, // ms between full poll cycles + DEBUG: false, +}; + +/* === REGISTER MAP === */ +var REG = { + CELLS_BASE: 0x1200, + MAIN_BASE: 0x128A, + MAIN_QTY: 30, +}; + +/* === ALARM BIT LABELS === */ +var ALARM_LABELS = [ + 'Cell undervoltage', // bit 0 + 'Cell overvoltage', // bit 1 + 'Discharge overcurrent', // bit 2 + 'Charge overcurrent', // bit 3 + 'Low temperature (chg)', // bit 4 + 'High temperature (dis)', // bit 5 + 'MOS overtemperature', // bit 6 + 'Short circuit', // bit 7 + 'Cell delta too large', // bit 8 + 'Pack undervoltage', // bit 9 + 'Pack overvoltage', // bit 10 + 'Low SOC', // bit 11 +]; + +/* === CRC-16 TABLE (MODBUS polynomial 0xA001) === */ +var CRC_TABLE = [ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040, +]; + +/* === STATE === */ +var state = { + uart: null, + rxBuffer: [], + isReady: false, + pendingRequest: null, + responseTimer: null, + pollTimer: null, +}; + +/* === HELPERS === */ + +function toHex(n) { + n = n & 0xFF; + return (n < 16 ? '0' : '') + n.toString(16).toUpperCase(); +} + +function bytesToHex(bytes) { + var s = ''; + for (var i = 0; i < bytes.length; i++) { + s += toHex(bytes[i]); + if (i < bytes.length - 1) s += ' '; + } + return s; +} + +function debug(msg) { + if (CONFIG.DEBUG) { + print('[JK200] ' + msg); + } +} + +function calcCRC(bytes) { + var crc = 0xFFFF; + for (var i = 0; i < bytes.length; i++) { + crc = (crc >> 8) ^ CRC_TABLE[(crc ^ bytes[i]) & 0xFF]; + } + return crc; +} + +function bytesToStr(bytes) { + var s = ''; + for (var i = 0; i < bytes.length; i++) { + s += String.fromCharCode(bytes[i] & 0xFF); + } + return s; +} + +function buildFrame(slaveAddr, functionCode, data) { + var frame = [slaveAddr & 0xFF, functionCode & 0xFF]; + for (var i = 0; i < data.length; i++) { + frame.push(data[i] & 0xFF); + } + var crc = calcCRC(frame); + frame.push(crc & 0xFF); + frame.push((crc >> 8) & 0xFF); + return frame; +} + +/* === SIGNED CONVERSIONS === */ + +function toSigned16(v) { + return v >= 0x8000 ? v - 0x10000 : v; +} + +function toSigned32(hi, lo) { + var v = hi * 65536 + lo; + return v >= 2147483648 ? v - 4294967296 : v; +} + +/* === DISPLAY FORMATTERS (integer arithmetic only) === */ + +function pad3(n) { + if (n < 10) return '00' + n; + if (n < 100) return '0' + n; + return '' + n; +} + +// millivolts → "X.XXX V" +function fmtV(mv) { + var sign = mv < 0 ? '-' : ''; + var abs = mv < 0 ? -mv : mv; + return sign + Math.floor(abs / 1000) + '.' + pad3(abs % 1000) + ' V'; +} + +// milliamps → "X.XXX A" +function fmtA(ma) { + var sign = ma < 0 ? '-' : ''; + var abs = ma < 0 ? -ma : ma; + return sign + Math.floor(abs / 1000) + '.' + pad3(abs % 1000) + ' A'; +} + +// milliwatts → "X.XXX W" +function fmtW(mw) { + var sign = mw < 0 ? '-' : ''; + var abs = mw < 0 ? -mw : mw; + return sign + Math.floor(abs / 1000) + '.' + pad3(abs % 1000) + ' W'; +} + +// 0.1 °C units → "X.X C" +function fmtC(tenths) { + var sign = tenths < 0 ? '-' : ''; + var abs = tenths < 0 ? -tenths : tenths; + return sign + Math.floor(abs / 10) + '.' + (abs % 10) + ' C'; +} + +/* === MODBUS CORE (FC 0x03 only) === */ + +var FC_READ_HOLDING = 0x03; + +function sendRequest(data, callback) { + if (!state.isReady) { + callback('Not initialised', null); + return; + } + if (state.pendingRequest) { + callback('Request pending', null); + return; + } + + var frame = buildFrame(CONFIG.SLAVE_ID, FC_READ_HOLDING, data); + debug('TX: ' + bytesToHex(frame)); + + state.pendingRequest = { callback: callback }; + state.rxBuffer = []; + + state.responseTimer = Timer.set(CONFIG.RESPONSE_TIMEOUT, false, function () { + if (state.pendingRequest) { + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + debug('Timeout'); + cb('Timeout', null); + } + }); + + state.uart.write(bytesToStr(frame)); +} + +function onReceive(data) { + if (!data || data.length === 0) return; + for (var i = 0; i < data.length; i++) { + state.rxBuffer.push(data.charCodeAt(i) & 0xFF); + } + processResponse(); +} + +function processResponse() { + if (!state.pendingRequest) { + state.rxBuffer = []; + return; + } + if (state.rxBuffer.length < 5) return; + + var fc = state.rxBuffer[1]; + + // Exception response + if (fc & 0x80) { + if (state.rxBuffer.length >= 5) { + var excCrc = calcCRC(state.rxBuffer.slice(0, 3)); + var recvCrc = state.rxBuffer[3] | (state.rxBuffer[4] << 8); + if (excCrc === recvCrc) { + clearResponseTimeout(); + var exCode = state.rxBuffer[2]; + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb('Exception 0x' + toHex(exCode), null); + } + } + return; + } + + // Normal FC 0x03 response: slave(1) + FC(1) + byteCount(1) + data(N) + CRC(2) + if (fc !== FC_READ_HOLDING || state.rxBuffer.length < 3) return; + + var expectedLen = 3 + state.rxBuffer[2] + 2; + if (state.rxBuffer.length < expectedLen) return; + + var frame = state.rxBuffer.slice(0, expectedLen); + var crc = calcCRC(frame.slice(0, expectedLen - 2)); + var recvCrc = frame[expectedLen - 2] | (frame[expectedLen - 1] << 8); + + if (crc !== recvCrc) { + debug('CRC error'); + return; + } + + debug('RX: ' + bytesToHex(frame)); + clearResponseTimeout(); + + var responseData = frame.slice(2, expectedLen - 2); + var cb = state.pendingRequest.callback; + state.pendingRequest = null; + state.rxBuffer = []; + cb(null, responseData); +} + +function clearResponseTimeout() { + if (state.responseTimer) { + Timer.clear(state.responseTimer); + state.responseTimer = null; + } +} + +// Read qty holding registers starting at addr. +// Callback receives (err, registers[]) where registers[] is an array of +// uint16 values, one per MODBUS register. +function readRegisters(addr, qty, callback) { + var data = [ + (addr >> 8) & 0xFF, + addr & 0xFF, + (qty >> 8) & 0xFF, + qty & 0xFF, + ]; + + sendRequest(data, function (err, response) { + if (err) { + callback(err, null); + return; + } + // response[0] = byteCount, response[1..byteCount] = register data (big-endian) + var byteCount = response[0]; + var regs = []; + for (var i = 1; i <= byteCount; i += 2) { + regs.push((response[i] << 8) | response[i + 1]); + } + callback(null, regs); + }); +} + +/* === PARSE CELL BLOCK (start 0x1200, qty CELL_COUNT * 2) === */ + +// Returns { cells[], minV, maxV, deltaV, minCell, maxCell } (all in mV). +function parseCellBlock(regs) { + var cells = []; + var minV = 65535; + var maxV = 0; + var minCell = 0; + var maxCell = 0; + + for (var i = 0; i < CONFIG.CELL_COUNT; i++) { + // stride-2: even indices are data, odd are padding + var mv = regs[i * 2]; + cells.push(mv); + if (mv < minV) { minV = mv; minCell = i + 1; } + if (mv > maxV) { maxV = mv; maxCell = i + 1; } + } + + return { + cells: cells, + minV: minV, + maxV: maxV, + deltaV: maxV - minV, + minCell: minCell, + maxCell: maxCell, + }; +} + +/* === PARSE MAIN BLOCK (start 0x128A, qty 30) === */ + +// Register offsets within the response (relative to 0x128A): +// [0] 0x128A MOSFET temp S_WORD 0.1 °C +// [1] 0x128B (padding) +// [2..5] 0x128C (reserved) +// [6..7] 0x1290 Pack voltage U_DWORD mV → (regs[6]<<16)|regs[7] +// [8..9] 0x1292 (padding) +// [10..11] 0x1294 Pack power S_DWORD mW +// [12..13] 0x1296 (padding) +// [14..15] 0x1298 Pack current S_DWORD mA +// [16..17] 0x129A (padding) +// [18] 0x129C Temp 1 S_WORD 0.1 °C +// [19] 0x129D (padding) +// [20] 0x129E Temp 2 S_WORD 0.1 °C +// [21] 0x129F (padding) +// [22..23] 0x12A0 Alarm bits U_DWORD bitmask +// [24..25] 0x12A2 (padding) +// [26] 0x12A4 Balance curr S_WORD mA +// [27] 0x12A5 (padding) +// [28] 0x12A6 SOC U_WORD % +// [29] 0x12A7 (padding) +function parseMainBlock(regs) { + return { + mosFetTemp: toSigned16(regs[0]), // 0.1 °C + voltage: regs[6] * 65536 + regs[7], // mV (U_DWORD) + power: toSigned32(regs[10], regs[11]), // mW (S_DWORD, + charge / - discharge) + current: toSigned32(regs[14], regs[15]), // mA (S_DWORD, + charge / - discharge) + temp1: toSigned16(regs[18]), // 0.1 °C + temp2: toSigned16(regs[20]), // 0.1 °C + alarms: regs[22] * 65536 + regs[23], // bitmask + balanceCurrent: toSigned16(regs[26]), // mA + soc: regs[28], // % + }; +} + +/* === PRINT === */ + +function printAlarms(bitmask) { + if (bitmask === 0) { + print(' Alarms: none'); + return; + } + var active = 'Alarms: '; + for (var b = 0; b < ALARM_LABELS.length; b++) { + if (bitmask & (1 << b)) { + active += '[' + ALARM_LABELS[b] + '] '; + } + } + if (bitmask & 0x8000) active += '[Manual shutdown] '; + print(' ' + active); +} + +function printData(cellData, main) { + print('--- JK200 BMS ---'); + + if (cellData) { + print(' Cells (' + CONFIG.CELL_COUNT + '):'); + for (var i = 0; i < cellData.cells.length; i++) { + var tag = ''; + if (i + 1 === cellData.minCell) tag = ' (min)'; + if (i + 1 === cellData.maxCell) tag = ' (max)'; + print(' ' + (i + 1 < 10 ? ' ' : '') + (i + 1) + ': ' + fmtV(cellData.cells[i]) + tag); + } + print(' Delta: ' + fmtV(cellData.deltaV) + + ' | Min: ' + fmtV(cellData.minV) + ' (cell ' + cellData.minCell + ')' + + ' | Max: ' + fmtV(cellData.maxV) + ' (cell ' + cellData.maxCell + ')'); + } else { + print(' Cells: read error'); + } + + if (main) { + print(' Pack: ' + fmtV(main.voltage) + + ' | ' + fmtA(main.current) + + ' | ' + fmtW(main.power)); + print(' SOC: ' + main.soc + ' %'); + print(' Temp: MOS ' + fmtC(main.mosFetTemp) + + ' | T1 ' + fmtC(main.temp1) + + ' | T2 ' + fmtC(main.temp2)); + print(' Balance: ' + fmtA(main.balanceCurrent)); + printAlarms(main.alarms); + } else { + print(' Main params: read error'); + } + + print(''); +} + +/* === POLL === */ + +function pollBMS() { + var cellQty = CONFIG.CELL_COUNT * 2; + + readRegisters(REG.CELLS_BASE, cellQty, function (err, regs) { + var cellData = null; + if (err) { + print('[JK200] Cell block error: ' + err); + } else { + if (regs.length < cellQty) { + print('[JK200] Cell block short (' + regs.length + ')'); + } else { + cellData = parseCellBlock(regs); + } + } + + Timer.set(CONFIG.INTER_READ_DELAY, false, function () { + readRegisters(REG.MAIN_BASE, REG.MAIN_QTY, function (err, regs) { + var main = null; + if (err) { + print('[JK200] Main block error: ' + err); + } else if (regs.length < REG.MAIN_QTY) { + print('[JK200] Main block short (' + regs.length + ')'); + } else { + main = parseMainBlock(regs); + } + + printData(cellData, main); + }); + }); + }); +} + +/* === INIT === */ + +function init() { + print('JK200 BMS - MODBUS-RTU Reader'); + print('=============================='); + + state.uart = UART.get(); + if (!state.uart) { + print('ERROR: UART not available'); + return; + } + + if (!state.uart.configure({ baud: CONFIG.BAUD_RATE, mode: CONFIG.MODE })) { + print('ERROR: UART configuration failed'); + return; + } + + state.uart.recv(onReceive); + state.isReady = true; + + debug('UART: ' + CONFIG.BAUD_RATE + ' baud, ' + CONFIG.MODE); + debug('Slave ID: ' + CONFIG.SLAVE_ID); + print('Cells: ' + CONFIG.CELL_COUNT + ' | Poll: ' + (CONFIG.POLL_INTERVAL / 1000) + ' s'); + print(''); + + Timer.set(500, false, pollBMS); + state.pollTimer = Timer.set(CONFIG.POLL_INTERVAL, true, pollBMS); +} + +init(); diff --git a/the_pill/MODBUS/README.md b/the_pill/MODBUS/README.md index 7cb3d92..9176826 100644 --- a/the_pill/MODBUS/README.md +++ b/the_pill/MODBUS/README.md @@ -66,40 +66,15 @@ MODBUS.readCoil(slave, addr, callback) // Read single coil --- -### mb308v.shelly.js +### ComWinTop/mb308v.shelly.js -**CWT-MB308V GPIO Expander Example** - Complete example for ComWinTop MB308V IO module. +**CWT-MB308V GPIO Expander Example** - See [ComWinTop/README.md](ComWinTop/README.md) for full documentation. -**Device Specifications:** -- 8 Analog Inputs (AI): 4-20mA / 0-5V / 0-10V -- 4 Analog Outputs (AO): 0-10V / 4-20mA -- 8 Digital Inputs (DI): Dry contact / NPN -- 12 Digital Outputs (DO): Relay outputs - -**Register Map:** +--- -| Type | Function Code | Address Range | Count | -|------|---------------|---------------|-------| -| Digital Outputs (DO) | FC 0x01 (read) / FC 0x05 (write) | 0-11 | 12 coils | -| Digital Inputs (DI) | FC 0x02 (read) | 0-7 | 8 inputs | -| Analog Outputs (AO) | FC 0x03 (read) / FC 0x06 (write) | 0-3 | 4 registers | -| Analog Inputs (AI) | FC 0x04 (read) | 0-7 | 8 registers | +### JK200-MBS/the_pill_mbsa_jk200.shelly.js -**API Methods:** -```javascript -readDigitalInputs(callback) // Read 8 DI -readDigitalOutputs(callback) // Read 12 DO (relays) -writeDigitalOutput(channel, value, cb) // Set relay (0-11, true/false) -readAnalogInputs(callback) // Read 8 AI -readAnalogOutputs(callback) // Read 4 AO -writeAnalogOutput(channel, value, cb) // Set AO (0-3, 0-24000) - -// Conversion helpers -aiToMilliamps(raw) // Convert AI to mA (4-20mA mode) -aiToVoltage(raw) // Convert AI to V (0-10V mode) -milliampsToAo(mA) // Convert mA to AO value -voltageToAo(volts) // Convert V to AO value -``` +**Jikong JK-PB BMS Reader** - See [JK200-MBS/README.md](JK200-MBS/README.md) for full documentation. ## Usage Examples @@ -143,54 +118,6 @@ MODBUS.writeSingleRegister(1, 100, 250, function(err, success) { }); ``` -### CWT-MB308V Examples - -**Read all digital inputs:** -```javascript -readDigitalInputs(function(err, inputs) { - if (err) { - print("Error: " + err); - return; - } - for (var i = 0; i < inputs.length; i++) { - print("DI" + i + ": " + (inputs[i] ? "ON" : "OFF")); - } -}); -``` - -**Control relay output:** -```javascript -// Turn ON relay 0 -writeDigitalOutput(0, true, function(err, success) { - if (success) print("Relay 0 ON"); -}); - -// Turn OFF relay 5 -writeDigitalOutput(5, false, function(err, success) { - if (success) print("Relay 5 OFF"); -}); -``` - -**Read analog inputs (4-20mA sensors):** -```javascript -readAnalogInputs(function(err, values) { - if (err) return; - for (var i = 0; i < values.length; i++) { - var mA = aiToMilliamps(values[i]); - print("AI" + i + ": " + mA.toFixed(2) + " mA"); - } -}); -``` - -**Set analog output (0-10V mode):** -```javascript -// Set AO0 to 5V -var rawValue = voltageToAo(5.0); -writeAnalogOutput(0, rawValue, function(err, success) { - if (success) print("AO0 set to 5V"); -}); -``` - ## Configuration ```javascript @@ -206,5 +133,3 @@ var CONFIG = { - [MODBUS Protocol Specification](https://modbus.org/specs.php) - [MODBUS over Serial Line](https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf) -- [ComWinTop CWT-MB308V](https://store.comwintop.com/products/cwt-mb308v-8ai-4ao-8di-12do-rs485-rs232-ethernet-modbus-rtu-tcp-io-acquisition-module) -- [MB308V Python Driver](https://github.com/bgerp/ztm/blob/master/Zontromat/devices/vendors/cwt/mb308v/mb308v.py) diff --git a/the_pill/README.md b/the_pill/README.md index 2a6b015..a00abab 100644 --- a/the_pill/README.md +++ b/the_pill/README.md @@ -11,4 +11,5 @@ its own README with wiring, setup, and usage details. - SDS011 air quality sensor (PM2.5/PM10) — *under development* - SDS018 air quality sensor (PM2.5/PM10) — *under development* - YS-IRTM infrared module (send/receive NEC codes) — *under development* +- UART test (loopback/wiring verification) — *under development* - _backup (deprecated/experimental scripts) diff --git a/the_pill/UART/README.md b/the_pill/UART/README.md new file mode 100644 index 0000000..12dad41 --- /dev/null +++ b/the_pill/UART/README.md @@ -0,0 +1,50 @@ +# UART Test + +> **Under Development** - This example is currently under development and may not be fully functional. + +Simple UART loopback test for verifying wiring and communication with The Pill. + +## Hardware Requirements + +- Shelly device with UART (e.g., The Pill) +- Any UART-compatible device or USB-to-UART adapter + +### Wiring + +| Device | Shelly | +|--------|--------| +| TX | RX (GPIO) | +| RX | TX (GPIO) | +| GND | GND | + +**UART Settings:** 9600 baud, 8N1 + +## Files + +### uart_test.shelly.js + +**UART Loopback Test** - Sends periodic messages and prints received data. + +**Features:** +- Sends "Hello UART" every 2 seconds +- Prints all received UART data +- Configurable baud rate, message, and interval +- Optional ACK (0xF1) filtering + +**Configuration:** +```javascript +var CONFIG = { + baud: 9600, // Baud rate + mode: '8N1', // UART mode + txInterval: 2000, // Send interval (ms) + txMessage: 'Hello UART', + debug: true +}; +``` + +## Quick Start + +1. Connect your UART device to Shelly (TX/RX/GND) +2. Upload `uart_test.shelly.js` to your Shelly device +3. Open the device console to see TX/RX messages +4. Verify data appears on both ends diff --git a/the_pill/UART/uart_test.shelly.js b/the_pill/UART/uart_test.shelly.js new file mode 100644 index 0000000..7c58e54 --- /dev/null +++ b/the_pill/UART/uart_test.shelly.js @@ -0,0 +1,81 @@ +/** + * @title UART test + * @description Simple UART loopback test that sends periodic messages and prints received data. + * @status production + * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/UART/uart_test.shelly.js + */ + +/** + * UART Test Script for Shelly (The Pill) + * + * Minimal UART test that sends "Hello UART" every 2 seconds and prints + * any received data. Useful for verifying UART wiring and communication. + * + * Hardware connection: + * - Device TX -> Shelly RX (GPIO) + * - Device RX -> Shelly TX (GPIO) + * - GND -> GND + * + * UART Settings: 9600 baud, 8N1 + */ + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +var CONFIG = { + baud: 9600, + mode: '8N1', + txInterval: 2000, + txMessage: 'Hello UART', + debug: true +}; + +// ============================================================================ +// STATE +// ============================================================================ + +var uart = null; + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +function dbg(msg) { + if (CONFIG.debug) print('[UART-TEST] ' + msg); +} + +// ============================================================================ +// MAIN LOGIC +// ============================================================================ + +function onReceive(data) { + if (!data || !data.length) return; + // Optional: ignore ACK F1 + if (data.length === 1 && (data.charCodeAt(0) & 0xff) === 0xf1) { + return; + } + dbg('RX: ' + data); +} + +function sendMessage() { + uart.write(CONFIG.txMessage); + dbg('TX: ' + CONFIG.txMessage); +} + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +function init() { + uart = UART.get(); + if (!uart.configure({ baud: CONFIG.baud, mode: CONFIG.mode })) { + print('[UART-TEST] ERROR: Failed to configure UART'); + die(); + } + uart.recv(onReceive); + Timer.set(CONFIG.txInterval, true, sendMessage); + dbg('Ready @ ' + CONFIG.baud + ' baud'); +} + +init(); diff --git a/the_pill/ys_irtm/README.md b/the_pill/ys_irtm/README.md index 3845d5b..d0884e9 100644 --- a/the_pill/ys_irtm/README.md +++ b/the_pill/ys_irtm/README.md @@ -1,7 +1,5 @@ # YS-IRTM Infrared Module -> **Under Development** - This example is currently under development and may not be fully functional. - Scripts for sending and receiving NEC-format infrared codes using the YS-IRTM module. ## Hardware Requirements diff --git a/the_pill/ys_irtm/btn2ir.shelly.js b/the_pill/ys_irtm/btn2ir.shelly.js index e153be4..54eed4b 100644 --- a/the_pill/ys_irtm/btn2ir.shelly.js +++ b/the_pill/ys_irtm/btn2ir.shelly.js @@ -1,7 +1,7 @@ /** * @title YS-IRTM button-to-IR example * @description Maps Shelly buttons and inputs to IR codes using YS-IRTM. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/btn2ir.shelly.js */ diff --git a/the_pill/ys_irtm/ir2sw.shelly.js b/the_pill/ys_irtm/ir2sw.shelly.js index 1970235..81308b8 100644 --- a/the_pill/ys_irtm/ir2sw.shelly.js +++ b/the_pill/ys_irtm/ir2sw.shelly.js @@ -1,7 +1,7 @@ /** * @title YS-IRTM IR-to-switch example * @description Maps received IR codes to Shelly switch actions. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/ir2sw.shelly.js */ diff --git a/the_pill/ys_irtm/ir_full.shelly.js b/the_pill/ys_irtm/ir_full.shelly.js index 3855603..c0e7d22 100644 --- a/the_pill/ys_irtm/ir_full.shelly.js +++ b/the_pill/ys_irtm/ir_full.shelly.js @@ -2,7 +2,7 @@ * @title YS-IRTM advanced IR automation example * @description Bidirectional IR automation with scenes, HTTP calls, and switch * integration. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/ir_full.shelly.js */ diff --git a/the_pill/ys_irtm/ir_learn.shelly.js b/the_pill/ys_irtm/ir_learn.shelly.js index f8ea5d8..f43db5f 100644 --- a/the_pill/ys_irtm/ir_learn.shelly.js +++ b/the_pill/ys_irtm/ir_learn.shelly.js @@ -1,7 +1,7 @@ /** * @title YS-IRTM IR learn mode * @description Captures and prints NEC IR codes for reuse in other scripts. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/ir_learn.shelly.js */ diff --git a/the_pill/ys_irtm/tv_ir.shelly.js b/the_pill/ys_irtm/tv_ir.shelly.js index eb91738..dddb8a1 100644 --- a/the_pill/ys_irtm/tv_ir.shelly.js +++ b/the_pill/ys_irtm/tv_ir.shelly.js @@ -1,7 +1,7 @@ /** * @title YS-IRTM TV remote codes * @description Preconfigured NEC IR codes for common TV brands using YS-IRTM. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/tv_ir.shelly.js */ diff --git a/the_pill/ys_irtm/ysirtm.shelly.js b/the_pill/ys_irtm/ysirtm.shelly.js index 14d4144..28caa5a 100644 --- a/the_pill/ys_irtm/ysirtm.shelly.js +++ b/the_pill/ys_irtm/ysirtm.shelly.js @@ -2,7 +2,7 @@ * @title YS-IRTM infrared UART library * @description UART protocol implementation for YS-IRTM to send and receive NEC IR * codes. - * @status under development + * @status production * @link https://github.com/ALLTERCO/shelly-script-examples/blob/main/the_pill/ys_irtm/ysirtm.shelly.js */ diff --git a/tools/put_script.py b/tools/put_script.py index 8688eca..3570d85 100755 --- a/tools/put_script.py +++ b/tools/put_script.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import json +import os import sys import urllib.request import urllib.error @@ -54,6 +55,12 @@ def start_script(host, script_id): call_rpc(host, "Script.Start", {"id": script_id}) +def rename_script(host, script_id, name): + """Set the script name on the device.""" + print(f"Setting name to '{name}'...") + call_rpc(host, "Script.SetConfig", {"id": script_id, "config": {"name": name}}) + + def upload_script(host, script_id, code): """Upload script code in chunks.""" total = len(code) @@ -81,7 +88,10 @@ def main(): with open(args.file, mode="r", encoding="utf-8") as f: code = f.read() + name = os.path.basename(args.file) + stop_script(args.host, args.id) + rename_script(args.host, args.id, name) upload_script(args.host, args.id, code) start_script(args.host, args.id) diff --git a/tools/sync-manifest-json.py b/tools/sync-manifest-json.py index 4709cd8..a24f996 100644 --- a/tools/sync-manifest-json.py +++ b/tools/sync-manifest-json.py @@ -15,10 +15,14 @@ import os import json -argparser = ArgumentParser() -argparser.add_argument("file", help="Path to the json file") +# Default paths (relative to this script's location) +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_REPO_ROOT = os.path.dirname(SCRIPT_DIR) +DEFAULT_MANIFEST = os.path.join(DEFAULT_REPO_ROOT, "examples-manifest.json") def main(): + argparser = ArgumentParser() + argparser.add_argument("file", nargs="?", default=DEFAULT_MANIFEST, help="Path to the json file (default: examples-manifest.json)") args = argparser.parse_args() if not args.file: print("Missing file argument")