Una repositoy che integra un plugin custom su SignalK per ottenere tutti i dati del BMS della batteria

- Introdotta l'implementazione JavaScript per la comunicazione BMS in bmscore.js, inclusi i metodi per il recupero dati e la gestione degli errori.
- Creato errors.js per mappare i codici di errore dal formato Python a quello JavaScript.
This commit is contained in:
Giuseppe Raffa
2026-05-11 19:45:07 +02:00
commit 4d5d51c018
11 changed files with 1664 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.DS_Store
node_modules
.env
.claude

150
index.js Normal file
View File

@@ -0,0 +1,150 @@
// SignalK plugin — Daly BMS via USB/RS485 (solo USB).
// Polling periodico → delta SignalK. POST read-only on-demand.
'use strict';
const DalyProtocol = require('./lib/daly-protocol');
const DEFAULTS = Object.freeze({
device: '/dev/ttyUSB0',
batteryId: 'house',
pollSocMs: 2000,
pollStatusMs: 30000,
pollCellsMs: 10000,
retries: 5
});
module.exports = function (app) {
const plugin = {
id: 'signalk-daly-bms',
name: 'Daly BMS (USB)',
description: 'Legge un BMS Daly via seriale USB/RS485 e pubblica su SignalK'
};
plugin.schema = {
type: 'object',
properties: {
device: { type: 'string', default: DEFAULTS.device, title: 'Serial device (USB)' },
batteryId: { type: 'string', default: DEFAULTS.batteryId, title: 'SignalK battery id' },
pollSocMs: { type: 'integer', default: DEFAULTS.pollSocMs, title: 'Polling SOC/V/A (ms)' },
pollStatusMs: { type: 'integer', default: DEFAULTS.pollStatusMs, title: 'Polling status (ms)' },
pollCellsMs: { type: 'integer', default: DEFAULTS.pollCellsMs, title: 'Polling celle/temperature (ms)' },
retries: { type: 'integer', default: DEFAULTS.retries, title: 'Retry per richiesta' }
}
};
let bms = null;
let opts = null;
const timers = new Set();
const sk = (path, value) => ({ path: `electrical.batteries.${opts.batteryId}.${path}`, value });
const sendDelta = values => values && values.length && app.handleMessage(plugin.id, { updates: [{ values }] });
const safe = async (label, fn) => {
try { return await fn(); }
catch (e) { app.error(`${label}: ${e.message}`); return null; }
};
// --- pollers ---
async function pollSoc() {
const s = await safe('pollSoc', () => bms.getSoc());
if (!s) return;
sendDelta([
sk('voltage', s.total_voltage),
sk('current', s.current), // SignalK: + = uscita (scarica), come Daly
sk('capacity.stateOfCharge', s.soc_percent / 100)
]);
}
async function pollStatus() {
const s = await safe('pollStatus', () => bms.getStatus());
if (!s) return;
sendDelta([
sk('cycles', s.cycles),
sk('cellsCount', s.cells),
sk('chargerRunning', s.charger_running),
sk('loadRunning', s.load_running)
]);
}
async function pollCells() {
const [cells, temps, errs] = await Promise.all([
safe('getCellVoltages', () => bms.getCellVoltages()),
safe('getTemperatures', () => bms.getTemperatures()),
safe('getErrors', () => bms.getErrors())
]);
const values = [];
if (cells) for (const [k, v] of Object.entries(cells)) values.push(sk(`cells.${k}.voltage`, v));
if (temps) {
const list = Object.values(temps);
const toK = c => Math.round((c + 273.15) * 100) / 100; // °C → K, 2 decimali
if (list.length) values.push(sk('temperature', toK(list[0])));
for (const [k, v] of Object.entries(temps)) values.push(sk(`temperatures.${k}`, toK(v)));
}
if (errs) values.push(sk('errors', errs));
sendDelta(values);
}
const startTimer = (fn, ms) => {
const t = setInterval(() => { fn(); }, ms);
timers.add(t);
};
plugin.start = async function (options = {}) {
opts = { ...DEFAULTS, ...options };
app.setPluginStatus(`Apertura ${opts.device}`);
bms = new DalyProtocol({
device: opts.device,
retries: opts.retries,
log: msg => app.debug(msg)
});
try {
await bms.open();
await bms.getStatus(); // necessario per cell/temp
app.setPluginStatus(`Connesso a ${opts.device}`);
} catch (e) {
app.setPluginError(`init failed: ${e.message}`);
return;
}
// primo giro immediato + polling periodico
await Promise.all([pollStatus(), pollSoc(), pollCells()]);
startTimer(pollSoc, opts.pollSocMs);
startTimer(pollStatus, opts.pollStatusMs);
startTimer(pollCells, opts.pollCellsMs);
};
plugin.stop = async function () {
for (const t of timers) clearInterval(t);
timers.clear();
if (bms) { await bms.close().catch(() => {}); bms = null; }
app.setPluginStatus('stopped');
};
// --- API REST: solo letture on-demand ---
plugin.registerWithRouter = function (router) {
const wrap = fn => async (_req, res) => {
if (!bms) return res.status(503).json({ error: 'plugin not started' });
try { res.json(await fn()); }
catch (e) { res.status(500).json({ error: e.message }); }
};
router.post('/soc', wrap(() => bms.getSoc()));
router.post('/status', wrap(() => bms.getStatus()));
router.post('/temperatures', wrap(() => bms.getTemperatures()));
router.post('/cell-voltages', wrap(() => bms.getCellVoltages()));
router.post('/cell-range', wrap(() => bms.getCellVoltageRange()));
router.post('/temp-range', wrap(() => bms.getTemperatureRange()));
router.post('/mosfet', wrap(() => bms.getMosfetStatus()));
router.post('/errors', wrap(() => bms.getErrors()));
router.post('/all', wrap(async () => {
const [soc, status, cell_voltages, temperatures, mosfet, errors] = await Promise.all([
bms.getSoc(), bms.getStatus(), bms.getCellVoltages(),
bms.getTemperatures(), bms.getMosfetStatus(), bms.getErrors()
]);
return { soc, status, cell_voltages, temperatures, mosfet, errors };
}));
};
return plugin;
};

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "meb-battery",
"version": "0.1.0",
"description": "Un plugin per il BMS della batteria",
"main": "index.js",
"keywords": [
"signalk-node-server-plugin",
"signalk-category-utility",
"daly",
"bms",
"battery"
],
"author": "MEB Team",
"license": "MIT",
"dependencies": {
"serialport": "^10.2.2"
}
}

View File

@@ -0,0 +1,7 @@
from .daly_bms import DalyBMS
from .daly_sinowealth import DalyBMSSinowealth
try:
from .daly_bms_bluetooth import DalyBMSBluetooth
except ImportError:
# Bluetooth is optional and requires bleak to be installed
pass

254
python-reference/daly-bms-cli Executable file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/python3
import argparse
import json
import logging
import sys
from dalybms import DalyBMS
from dalybms import DalyBMSSinowealth
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--device",
help="RS485 device, e.g. /dev/ttyUSB0",
type=str, required=True)
parser.add_argument("--uart", help="UART instead of RS485", action="store_true")
parser.add_argument("--sinowealth", help="BMS with Sinowealth chip", action="store_true")
parser.add_argument("--status", help="show status", action="store_true")
parser.add_argument("--soc", help="show voltage, current, SOC", action="store_true")
parser.add_argument("--mosfet", help="show mosfet status", action="store_true")
parser.add_argument("--cell-voltages", help="show cell voltages", action="store_true")
parser.add_argument("--temperatures", help="show temperature sensor values", action="store_true")
parser.add_argument("--balancing", help="show cell balancing status", action="store_true")
parser.add_argument("--errors", help="show BMS errors", action="store_true")
parser.add_argument("--all", help="show all", action="store_true")
parser.add_argument("--check", help="Nagios style check", action="store_true")
parser.add_argument("--set-charge-mosfet", help="'on' or 'off'", type=str)
parser.add_argument("--set-discharge-mosfet", help="'on' or 'off'", type=str)
parser.add_argument("--set-soc", help="'0.0' to '100.0'", type=str)
parser.add_argument("--restart", help="restart bms", action="store_true")
parser.add_argument("--retry", help="retry X times if the request fails, default 5", type=int, default=5)
parser.add_argument("--verbose", help="Verbose output", action="store_true")
parser.add_argument("--mqtt", help="Write output to MQTT", action="store_true")
parser.add_argument("--mqtt-hass", help="MQTT Home Assistant Mode", action="store_true")
parser.add_argument("--mqtt-topic",
help="MQTT topic to write to. default daly_bms",
type=str,
default="daly_bms")
parser.add_argument("--mqtt-broker",
help="MQTT broker (server). default localhost",
type=str,
default="localhost")
parser.add_argument("--mqtt-port",
help="MQTT port. default 1883",
type=int,
default=1883)
parser.add_argument("--mqtt-user",
help="Username to authenticate MQTT with",
type=str)
parser.add_argument("--mqtt-password",
help="Password to authenticate MQTT with",
type=str)
args = parser.parse_args()
log_format = '%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s'
if args.verbose:
level = logging.DEBUG
else:
level = logging.WARNING
logging.basicConfig(level=level, format=log_format, datefmt='%H:%M:%S')
logger = logging.getLogger()
if args.uart:
address = 8
else:
address = 4
if args.sinowealth:
bms = DalyBMSSinowealth(request_retries=args.retry, logger=logger)
else:
bms = DalyBMS(request_retries=args.retry, address=address, logger=logger)
bms.connect(device=args.device)
result = False
mqtt_client = None
if args.mqtt:
import paho.mqtt.client as paho
mqtt_client = paho.Client()
mqtt_client.enable_logger(logger)
mqtt_client.username_pw_set(args.mqtt_user, args.mqtt_password)
mqtt_client.connect(args.mqtt_broker, port=args.mqtt_port)
def build_mqtt_hass_config_discovery(base):
# Instead of daly_bms should be here added a proper name (unique), like serial or something
# At this point it can be used only one daly_bms system with hass discovery
hass_config_topic = f'homeassistant/sensor/daly_bms/{base.replace("/", "_")}/config'
hass_config_data = {}
hass_config_data["unique_id"] = f'daly_bms_{base.replace("/", "_")}'
hass_config_data["name"] = f'Daly BMS {base.replace("/", " ")}'
if 'soc_percent' in base:
hass_config_data["device_class"] = 'battery'
hass_config_data["unit_of_measurement"] = '%'
elif 'voltage' in base and not ('lowest_cell' in base or 'highest_cell' in base):
hass_config_data["device_class"] = 'voltage'
hass_config_data["unit_of_measurement"] = 'V'
elif 'current' in base:
hass_config_data["device_class"] = 'current'
hass_config_data["unit_of_measurement"] = 'A'
elif 'temperatures' in base:
hass_config_data["device_class"] = 'temperature'
hass_config_data["unit_of_measurement"] = '°C'
elif 'capacity' in 'base':
hass_config_data["device_class"] = 'energy'
hass_config_data["unit_of_measurement"] = 'Ah'
else:
pass
hass_config_data["json_attributes_topic"] = f'{args.mqtt_topic}{base}'
hass_config_data["state_topic"] = f'{args.mqtt_topic}{base}'
hass_device = {
"identifiers": ['daly_bms'],
"manufacturer": 'Daly',
"model": 'Currently not available',
"name": 'Daly BMS',
"sw_version": 'Currently not available'
}
hass_config_data["device"] = hass_device
return hass_config_topic, json.dumps(hass_config_data)
def mqtt_single_out(topic, data, retain=False):
logger.debug(f'Send data: {data} on topic: {topic}, retain flag: {retain}')
mqtt_client.publish(topic, data, retain=retain)
def mqtt_iterator(result, base=''):
for key in result.keys():
if type(result[key]) == dict:
mqtt_iterator(result[key], f'{base}/{key}')
else:
if args.mqtt_hass:
logger.debug('Sending out hass discovery message')
topic, output = build_mqtt_hass_config_discovery(f'{base}/{key}')
mqtt_single_out(topic, output, retain=True)
if type(result[key]) == list:
val = json.dumps(result[key])
else:
val = result[key]
mqtt_single_out(f'{args.mqtt_topic}{base}/{key}', val)
def print_result(result):
if args.mqtt:
mqtt_iterator(result)
else:
print(json.dumps(result, indent=2))
if args.status:
result = bms.get_status()
print_result(result)
if args.soc:
result = bms.get_soc()
print_result(result)
if args.mosfet:
result = bms.get_mosfet_status()
print_result(result)
if args.cell_voltages:
if not args.status:
bms.get_status()
result = bms.get_cell_voltages()
print_result(result)
if args.temperatures:
result = bms.get_temperatures()
print_result(result)
if args.balancing:
result = bms.get_balancing_status()
print_result(result)
if args.errors:
result = bms.get_errors()
print_result(result)
if args.all:
result = bms.get_all()
print_result(result)
if args.check:
status = bms.get_status()
status_code = 0 # OK
status_codes = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN')
status_line = ''
data = bms.get_soc()
perfdata = []
if data:
for key, value in data.items():
perfdata.append('%s=%s' % (key, value))
# todo: read errors
if status_code == 0:
status_line = '%0.1f volt, %0.1f amper' % (data['total_voltage'], data['current'])
print("%s - %s | %s" % (status_codes[status_code], status_line, " ".join(perfdata)))
sys.exit(status_code)
if args.set_charge_mosfet:
if args.set_charge_mosfet == 'on':
on = True
elif args.set_charge_mosfet == 'off':
on = False
else:
print("invalid value '%s', expected 'on' or 'off'" % args.set_charge_mosfet)
sys.exit(1)
result = bms.set_charge_mosfet(on=on)
if args.set_discharge_mosfet:
if args.set_discharge_mosfet == 'on':
on = True
elif args.set_discharge_mosfet == 'off':
on = False
else:
print("invalid value '%s', expected 'on' or 'off'" % args.set_discharge_mosfet)
sys.exit(1)
result = bms.set_discharge_mosfet(on=on)
if args.set_soc:
try :
v = float(args.set_soc)
except :
print("invalid value '%s', expected float value betwen 0 and 100" % args.set_soc)
sys.exit(1)
result = bms.set_soc(v)
if args.restart:
result = bms.restart()
if mqtt_client:
mqtt_client.disconnect()
bms.disconnect()
if not result:
sys.exit(1)

View File

@@ -0,0 +1,403 @@
import serial
import struct
import time
import math
import logging
from .error_codes import ERROR_CODES
class DalyBMS:
def __init__(self, request_retries=3, address=4, logger=None):
"""
:param request_retries: How often read requests should get repeated in case that they fail (Default: 3).
:param address: Source address for commands sent to the BMS (4 for RS485, 8 for UART/Bluetooth)
:param logger: Python Logger object for output (Default: None)
"""
self.status = None
if logger:
self.logger = logger
else:
self.logger = logging.getLogger(__name__)
self.request_retries = request_retries
self.address = address # 4 = USB, 8 = Bluetooth
def connect(self, device):
"""
Connect to a serial device
:param device: Serial device, e.g. /dev/ttyUSB0
"""
self.serial = serial.Serial(
port=device,
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0.5,
xonxoff=False,
writeTimeout=0.5
)
self.get_status()
def disconnect(self):
if self.serial and self.serial.is_open:
self.serial.close()
@staticmethod
def _calc_crc(message_bytes):
"""
Calculate the checksum of a message
:param message_bytes: Bytes for which the checksum should get calculated
:return: Checksum as bytes
"""
return bytes([sum(message_bytes) & 0xFF])
def _format_message(self, command, extra=""):
"""
Takes the command ID and formats a request message
:param command: Command ID ("90" - "98")
:return: Request message as bytes
"""
# 95 -> a58095080000000000000000c2
message = "a5%i0%s08%s" % (self.address, command, extra)
message = message.ljust(24, "0")
message_bytes = bytearray.fromhex(message)
message_bytes += self._calc_crc(message_bytes)
self.logger.debug("w %s" % message_bytes.hex())
return message_bytes
def _read_request(self, command, extra="", max_responses=1, return_list=False):
"""
Sends a read request to the BMS and reads the response. In case it fails, it retries 'max_responses' times.
:param command: Command ID ("90" - "98")
:param max_responses: For how many response packages it should wait (Default: 1).
:return: Request message as bytes or False
"""
response_data = None
x = None
for x in range(0, self.request_retries):
response_data = self._read(
command=command,
extra=extra,
max_responses=max_responses,
return_list=return_list)
if not response_data:
self.logger.debug("%x. try failed, retrying..." % (x + 1))
time.sleep(0.2)
else:
break
if not response_data:
self.logger.error('%s failed after %s tries' % (command, x + 1))
return False
return response_data
def _read(self, command, extra="", max_responses=1, return_list=False):
self.logger.debug("-- %s ------------------------" % command)
if not self.serial.is_open:
self.serial.open()
message_bytes = self._format_message(command, extra=extra)
# clear all buffers, in case something is left from a previous command that failed
self.serial.reset_input_buffer()
self.serial.reset_output_buffer()
if not self.serial.write(message_bytes):
self.logger.error("serial write failed for command" % command)
return False
x = 0
response_data = []
while True:
b = self.serial.read(13)
if len(b) == 0:
self.logger.debug("%i empty response for command %s" % (x, command))
break
self.logger.debug("%i %s %s" % (x, b.hex(), len(b)))
x += 1
response_crc = self._calc_crc(b[:-1])
if response_crc != b[-1:]:
self.logger.debug("response crc mismatch: %s != %s" % (response_crc.hex(), b[-1:].hex()))
header = b[0:4].hex()
# todo: verify more header fields
if header[4:6] != command:
self.logger.debug("invalid header %s: wrong command (%s != %s)" % (header, header[4:6], command))
continue
data = b[4:-1]
response_data.append(data)
if x == max_responses:
break
if return_list or len(response_data) > 1:
return response_data
elif len(response_data) == 1:
return response_data[0]
else:
return False
def get_soc(self, response_data=None):
# SOC of Total Voltage Current
if not response_data:
response_data = self._read_request("90")
if not response_data:
return False
parts = struct.unpack('>h h h h', response_data)
data = {
"total_voltage": parts[0] / 10,
# "x_voltage": parts[1] / 10, # always 0
"current": (parts[2] - 30000) / 10, # negative=charging, positive=discharging
"soc_percent": parts[3] / 10
}
return data
def get_cell_voltage_range(self, response_data=None):
# Cells with the maximum and minimum voltage
if not response_data:
response_data = self._read_request("91")
if not response_data:
return False
parts = struct.unpack('>h b h b 2x', response_data)
data = {
"highest_voltage": parts[0] / 1000,
"highest_cell": parts[1],
"lowest_voltage": parts[2] / 1000,
"lowest_cell": parts[3],
}
return data
def get_temperature_range(self, response_data=None):
# Temperature in degrees celsius
if not response_data:
response_data = self._read_request("92")
if not response_data:
return False
parts = struct.unpack('>b b b b 4x', response_data)
data = {
"highest_temperature": parts[0] - 40,
"highest_sensor": parts[1],
"lowest_temperature": parts[2] - 40,
"lowest_sensor": parts[3],
}
return data
def get_mosfet_status(self, response_data=None):
# Charge/discharge, MOS status
if not response_data:
response_data = self._read_request("93")
if not response_data:
return False
# todo: implement
self.logger.debug(response_data.hex())
parts = struct.unpack('>b ? ? B l', response_data)
if parts[0] == 0:
mode = "stationary"
elif parts[0] == 1:
mode = "charging"
else:
mode = "discharging"
data = {
"mode": mode,
"charging_mosfet": parts[1],
"discharging_mosfet": parts[2],
# "bms_cycles": parts[3], unstable result
"capacity_ah": parts[4] / 1000,
}
return data
def get_status(self, response_data=None):
if not response_data:
response_data = self._read_request("94")
if not response_data:
return False
parts = struct.unpack('>b b ? ? b h x', response_data)
state_bits = bin(parts[4])[2:]
state_names = ["DI1", "DI2", "DI3", "DI4", "DO1", "DO2", "DO3", "DO4"]
states = {}
state_index = 0
for bit in reversed(state_bits):
if len(state_bits) == state_index:
break
states[state_names[state_index]] = bool(int(bit))
state_index += 1
data = {
"cells": parts[0], # number of cells
"temperature_sensors": parts[1], # number of sensors
"charger_running": parts[2],
"load_running": parts[3],
# "state_bits": state_bits,
"states": states,
"cycles": parts[5], # number of charge/discharge cycles
}
self.status = data
return data
def _calc_num_responses(self, status_field, num_per_frame):
if not self.status:
self.logger.error("get_status has to be called at least once before calling get_cell_voltages")
return False
# each response message includes 3 cell voltages
if self.address == 8:
# via Bluetooth the BMS returns all frames, even when they don't have data
if status_field == 'cell_voltages':
max_responses = 16
elif status_field == 'temperatures':
max_responses = 3
else:
self.logger.error("unkonwn status_field %s" % status_field)
return False
else:
# via UART/USB the BMS returns only frames that have data
max_responses = math.ceil(self.status[status_field] / num_per_frame)
return max_responses
def _split_frames(self, response_data, status_field, structure):
values = {}
x = 1
for response_bytes in response_data:
parts = struct.unpack(structure, response_bytes)
if parts[0] != x:
self.logger.warning("frame out of order, expected %i, got %i" % (x, response_bytes[0]))
continue
for value in parts[1:]:
values[len(values) + 1] = value
if len(values) == self.status[status_field]:
return values
x += 1
def get_cell_voltages(self, response_data=None):
if not response_data:
max_responses = self._calc_num_responses(status_field="cells", num_per_frame=3)
if not max_responses:
return
response_data = self._read_request("95", max_responses=max_responses, return_list=True)
if not response_data:
return False
cell_voltages = self._split_frames(response_data=response_data, status_field="cells", structure=">b 3h x")
for id in cell_voltages:
cell_voltages[id] = cell_voltages[id] / 1000
return cell_voltages
def get_temperatures(self, response_data=None):
# Sensor temperatures
if not response_data:
max_responses = self._calc_num_responses(status_field="temperature_sensors", num_per_frame=7)
if not max_responses:
return
response_data = self._read_request("96", max_responses=max_responses, return_list=True)
if not response_data:
return False
temperatures = self._split_frames(response_data=response_data, status_field="temperature_sensors",
structure=">b 7b")
for id in temperatures:
temperatures[id] = temperatures[id] - 40
return temperatures
def get_balancing_status(self, response_data=None):
# Cell balancing status
if not response_data:
response_data = self._read_request("97")
if not response_data:
return False
self.logger.info(response_data.hex())
bits = bin(int(response_data.hex(), base=16))[2:].zfill(48)
self.logger.info(bits)
cells = {}
for cell in range(1, self.status["cells"] + 1):
cells[cell] = bool(int(bits[cell * -1]))
self.logger.info(cells)
# todo: get sample data and verify result
return {"error": "not implemented"}
def get_errors(self, response_data=None):
# Battery failure status
if not response_data:
response_data = self._read_request("98")
if int.from_bytes(response_data, byteorder='big') == 0:
return []
byte_index = 0
errors = []
for b in response_data:
if b == 0:
byte_index += 1
continue
bits = bin(b)[2:]
bit_index = 0
for bit in reversed(bits):
if bit == "1":
errors.append(ERROR_CODES[byte_index][bit_index])
bit_index += 1
self.logger.debug("%s %s %s" % (byte_index, b, bits))
byte_index += 1
return errors
def get_all(self):
return {
"soc": self.get_soc(),
"cell_voltage_range": self.get_cell_voltage_range(),
"temperature_range": self.get_temperature_range(),
"mosfet_status": self.get_mosfet_status(),
"status": self.get_status(),
"cell_voltages": self.get_cell_voltages(),
"temperatures": self.get_temperatures(),
"balancing_status": self.get_balancing_status(),
"errors": self.get_errors()
}
def set_charge_mosfet(self, on=True, response_data=None):
if on:
extra = "01"
else:
extra = "00"
if not response_data:
response_data = self._read_request("da", extra=extra)
if not response_data:
return False
self.logger.info(response_data.hex())
# on response
# 0101000002006cbe
# off response
# 0001000002006c44
def set_discharge_mosfet(self, on=True, response_data=None):
if on:
extra = "01"
else:
extra = "00"
if not response_data:
response_data = self._read_request("d9", extra=extra)
if not response_data:
return False
self.logger.info(response_data.hex())
# on response
# 0101000002006cbe
# off response
# 0001000002006c44
# Set SoC. Value is float from 0.0 to 100.0
def set_soc(self, value):
v = round(value*10.0)
if v > 1000 : v = 1000
if v < 0 : v = 0
extra='000000000000%0.4X' % v
response_data = self._read_request("21", extra=extra)
self.logger.info(response_data.hex())
def restart(self, response_data=None):
response_data = self._read("00","",1,False)

View File

@@ -0,0 +1,180 @@
import asyncio
import subprocess
import logging
from bleak import BleakClient
from .daly_bms import DalyBMS
class DalyBMSBluetooth(DalyBMS):
def __init__(self, request_retries=3, logger=None):
"""
:param request_retries: How often read requests should get repeated in case that they fail (Default: 3).
:param logger: Python Logger object for output (Default: None)
"""
if logger:
self.logger = logger
else:
self.logger = logging.getLogger(__name__)
DalyBMS.__init__(self, request_retries=request_retries, address=8, logger=logger)
self.client = None
self.response_cache = {}
async def connect(self, mac_address):
"""
Open the connection to the Bluetooth device.
:param mac_address: MAC address of the Bluetooth device
"""
try:
"""
When an earlier execution of the script crashed, the connection to the devices stays open and future
connection attempts would fail with this error:
bleak.exc.BleakError: Device with address AA:BB:CC:DD:EE:FF was not found.
see https://github.com/hbldh/bleak/issues/367
"""
open_blue = subprocess.Popen(["bluetoothctl"], shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
open_blue.communicate(b"disconnect %s\n" % mac_address.encode('utf-8'))
open_blue.kill()
except:
pass
self.client = BleakClient(mac_address)
await self.client.connect()
await self.client.start_notify(17, self._notification_callback)
await self.client.write_gatt_char(48, bytearray(b""))
async def disconnect(self):
"""
Disconnect from the Bluetooth device
"""
self.logger.info("Bluetooth Disconnecting")
await self.client.disconnect()
self.logger.info("Bluetooth Disconnected")
async def _read_request(self, command, max_responses=1):
response_data = None
x = None
for x in range(0, self.request_retries):
response_data = await self._read(
command=command,
max_responses=max_responses)
if not response_data:
self.logger.debug("%x. try failed, retrying..." % (x + 1))
await asyncio.sleep(0.2)
else:
break
if not response_data:
self.logger.error('%s failed after %s tries' % (command, x + 1))
return False
return response_data
async def _read(self, command, max_responses=1):
self.logger.debug("-- %s ------------------------" % command)
self.response_cache[command] = {"queue": [],
"future": asyncio.Future(),
"max_responses": max_responses,
"done": False}
message_bytes = self._format_message(command)
result = await self._async_char_write(command, message_bytes)
self.logger.debug("got %s" % result)
if not result:
return False
if max_responses == 1:
return result[0]
else:
return result
def _notification_callback(self, handle, data):
self.logger.debug("%s %s %s" % (handle, repr(data), len(data)))
responses = []
if len(data) == 13:
responses.append(data)
elif len(data) == 26:
responses.append(data[0:13])
responses.append(data[13:])
else:
self.logger.error(len(data), "bytes received, not 13 or 26, not implemented")
for response_bytes in responses:
command = response_bytes[2:3].hex()
if self.response_cache[command]["done"] is True:
self.logger.debug("skipping response for %s, done" % command)
return
self.response_cache[command]["queue"].append(response_bytes[4:-1])
if len(self.response_cache[command]["queue"]) == self.response_cache[command]["max_responses"]:
self.response_cache[command]["done"] = True
self.response_cache[command]["future"].set_result(self.response_cache[command]["queue"])
async def _async_char_write(self, command, value):
if not self.client.is_connected:
self.logger.info("Connecting...")
await self.client.connect()
await self.client.write_gatt_char(15, value)
self.logger.debug("Waiting...")
try:
result = await asyncio.wait_for(self.response_cache[command]["future"], 5)
except asyncio.TimeoutError:
self.logger.warning("Timeout while waiting for %s response" % command)
return False
self.logger.debug("got %s" % result)
return result
# wrap all sync functions so that they can be awaited
async def get_soc(self, response_data=None):
response_data = await self._read_request("90")
return super().get_soc(response_data=response_data)
async def get_cell_voltage_range(self, response_data=None):
response_data = await self._read_request("91")
return super().get_cell_voltage_range(response_data=response_data)
async def get_max_min_temperature(self, response_data=None):
response_data = await self._read_request("92")
return super().get_max_min_temperature(response_data=response_data)
async def get_mosfet_status(self, response_data=None):
response_data = await self._read_request("93")
return super().get_mosfet_status(response_data=response_data)
async def get_status(self, response_data=None):
response_data = await self._read_request("94")
return super().get_status(response_data=response_data)
async def get_cell_voltages(self, response_data=None):
if not self.status:
await self.get_status()
max_responses = self._calc_cell_voltage_responses()
if not max_responses:
return
response_data = await self._read_request("95", max_responses=max_responses)
return super().get_cell_voltages(response_data=response_data)
async def get_temperatures(self, response_data=None):
response_data = await self._read_request("95")
return super().get_temperatures(response_data=response_data)
async def get_balancing_status(self, response_data=None):
response_data = await self._read_request("96")
return super().get_balancing_status(response_data=response_data)
async def get_errors(self, response_data=None):
response_data = await self._read_request("97")
return super().get_errors(response_data=response_data)
async def get_all(self):
return {
"soc": await self.get_soc(),
"cell_voltage_range": await self.get_cell_voltage_range(),
"temperature_range": await self.get_temperature_range(),
"mosfet_status": await self.get_mosfet_status(),
"status": await self.get_status(),
"cell_voltages": await self.get_cell_voltages(),
"temperatures": await self.get_temperatures(),
"balancing_status": await self.get_balancing_status(),
"errors": await self.get_errors()
}

View File

@@ -0,0 +1,241 @@
import serial
import struct
import logging
"""
List from BMStool PC / Sinowealth
1 = Cell 1 Voltage
...
09 = Cell 9 Voltage
0A = Cell 10 Voltage
0B = Total Voltage
0C = External Temperature 1
0D = External Temperature 2
0E = IC Temperature 1
0F = IC Temperature 2
10 = CADC Current (4 byte)
11 = Full Charge Capacity (4 byte)
12 = Remaining Capacity (4 byte)
13 = RSOC
14 = Cycle Count
15 = Pack Status
16 = Battery Status
17 = Pack Config
18 = Manufacture Access
"""
class DalyBMSSinowealth:
PACK_STATUS = {
0: 'CAL: ',
5: 'VDQ: Valid Discharge Qualified',
6: 'FD: Fully Discharged',
7: 'FC: Fully Charged',
9: 'FAST_DSG: Fast Discharging',
10: 'MID_DSG: Medium Discharging',
11: 'SLOW_DSG: Slow Discharging',
12: 'DSGING: Discharging',
13: 'CHGING: Charging',
14: 'DSGMOS: Discharging enabled',
15: 'CHGMOS: Charging enabled',
}
BATTERY_STATUS = {
1: 'CTO: Disconnection protection occurs',
2: 'AFE_SC: Hardware short circuit protection occurs',
3: 'AFE_OV: Hardware overvoltage protection occurs',
4: 'UTD: Discharge low temperature protection',
5: 'UTC: Charge low temperature protection occurs',
6: 'OTD: Discharge high temperature protection',
7: 'OTC: Charge high temperature protection occurs',
12: 'OCD: Discharge overcurrent protection occurs',
13: 'OCC: Charge overcurrent protection occurs',
14: 'UV: Undervoltage protection occurs',
15: 'OV: Overvoltage protection occurs',
}
def __init__(self, request_retries=3, logger=None):
"""
:param request_retries: How often read requests should get repeated in case that they fail (Default: 3).
:param logger: Python Logger object for output (Default: None)
"""
if logger:
self.logger = logger
else:
self.logger = logging.getLogger(__name__)
self.request_retries = request_retries
def connect(self, device):
"""
Connect to a serial device
:param device: Serial device, e.g. /dev/ttyUSB0
"""
self.serial = serial.Serial(
port=device,
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0.5,
xonxoff=False,
writeTimeout=0.5
)
def disconnect(self):
if self.serial and self.serial.is_open:
self.serial.close()
def _format_message(self, command, length):
message = "0a%s0%s" % (command.zfill(2), length)
message_bytes = bytearray.fromhex(message)
self.logger.debug("message: %s, %s" % (message_bytes, message_bytes.hex()))
return message_bytes
def _read(self, command):
if not self.serial.is_open:
self.serial.open()
if command in ("10", "11", "12"):
length = 4
else:
length = 2
message_bytes = self._format_message(command, length)
# clear all buffers, in case something is left from a previous command that failed
self.serial.reset_input_buffer()
self.serial.reset_output_buffer()
if not self.serial.write(message_bytes):
self.logger.error("serial write failed for command" % command)
return False
response_data = self.serial.read(length + 1)
if len(response_data) == 0:
self.logger.debug("empty response for command %s" % (command))
return False
self.logger.debug("%s (%i)" % (response_data.hex(), len(response_data)))
if command in ("10", "11", "12"):
return struct.unpack('>i x', response_data)[0]
elif command in ("15", "16", "17", "18"):
return bin(int.from_bytes(response_data[:-1], byteorder='big'))[2:].zfill(16)
else:
return struct.unpack('>h x', response_data)[0]
def get_cell_voltages(self):
max_cells = 10
x = 1
cell_voltages = {}
while x <= max_cells:
response_data = self._read("%02x" % x)
if not response_data:
break
if response_data == 0:
# last cell
break
cell_voltages[x] = response_data / 1000
x += 1
return cell_voltages
def _read_bulk(self, requests):
data = {}
for key, command in requests.items():
response_data = self._read(command[0])
if response_data is False:
continue
data[key] = response_data / command[1]
return data
def get_soc(self):
requests = {
"total_voltage": ("b", 1000),
"current": ("10", 1000),
"soc_percent": ("13", 1)
}
return self._read_bulk(requests)
def get_temperatures(self):
# The BMS returns temperatures in Kelvin
# 2731 / 10 = 273,1 K = 0°C
requests = {
"external1": ("c", 10),
"external2": ("d", 10),
# "ic1": ("e", 10),
# "ic2": ("f", 100), # always 71
}
responses = self._read_bulk(requests)
for key, value in responses.items():
# change temperatures from Kelvin to °C
responses[key] = round(value - 273, 2)
return responses
def get_status(self):
requests = {
"cycles": ("14", 1),
}
responses = self._read_bulk(requests)
for key, value in responses.items():
if type(responses[key]) is float:
responses[key] = int(value)
return responses
def get_mosfet_status(self):
requests = {
"full_capacity_ah": ("11", 1000),
"remaining_capacity_ah": ("12", 1000),
}
responses = self._read_bulk(requests)
for key, value in responses.items():
if type(responses[key]) is float:
responses[key] = round(value, 2)
pack_response = self._read("15")
if pack_response is False:
return responses
pack_state = []
for key, value in self.PACK_STATUS.items():
if pack_response[key] == "1":
pack_state.append(value)
responses['pack_state'] = pack_state
return responses
def get_errors(self):
response = self._read("16")
pack_state = []
for key, value in self.BATTERY_STATUS.items():
if response[key] == "1":
pack_state.append(value)
return pack_state
# dummy functions for everything that is not supported by the Sinowealth BMS
def get_cell_voltage_range(self):
return {}
def get_temperature_range(self):
return {}
def get_balancing_status(self):
return {}
def get_all(self):
return {
"soc": self.get_soc(),
# "cell_voltage_range": self.get_cell_voltage_range(),
# "temperature_range": self.get_temperature_range(),
"mosfet_status": self.get_mosfet_status(),
"status": self.get_status(),
"cell_voltages": self.get_cell_voltages(),
"temperatures": self.get_temperatures(),
# "balancing_status": self.get_balancing_status(),
"errors": self.get_errors()
}

View File

@@ -0,0 +1,67 @@
"""
The error messages are taken from the "Part 4_ Daly RS485+UART Protocol.pdf",
so the translation quality isn't that great yet.
"""
ERROR_CODES = {
0: [
"one stage warning of unit over voltage",
"one stage warning of unit over voltage",
"one stage warning of unit over voltage",
"two stage warning of unit over voltage",
"Total voltage is too high One alarm",
"Total voltage is too high Level two alarm",
"Total voltage is too low One alarm",
"Total voltage is too low Level two alarm"
],
1: ["Charging temperature too high. One alarm",
"Charging temperature too high. Level two alarm",
"Charging temperature too low. One alarm",
"Charging temperature's too low. Level two alarm",
"Discharge temperature is too high. One alarm",
"Discharge temperature is too high. Level two alarm",
"Discharge temperature is too low. One alarm",
"Discharge temperature is too low. Level two alarm",
],
2: ["Charge over current. Level one alarm",
"Charge over current, level two alarm",
"Discharge over current. Level one alarm",
"Discharge overcurrent, level two alarm",
"SOC is too high an alarm",
"SOC is too high. Alarm Two",
"SOC is too low. level one alarm",
"SOC is too low. level two alarm",
],
3: ["Excessive differential pressure level one alarm",
"Excessive differential pressure level two alarm",
"Excessive temperature difference level one alarm",
"Excessive temperature difference level two alarm",
],
4: ["charging MOS overtemperature warning",
"discharge MOS overtemperature warning",
"charging MOS temperature detection sensor failure",
"discharge MOS temperature detection sensor failure",
"charging MOS adhesion failure",
"discharge MOS adhesion failure",
"charging MOS breaker failure",
"discharge MOS breaker failure",
],
5: ["AFE acquisition chip malfunction",
"monomer collect drop off",
"Single Temperature Sensor Fault",
"EEPROM storage failures",
"RTC clock malfunction",
"Precharge Failure",
"vehicle communications malfunction",
"intranet communication module malfunction",
],
6: ["Current Module Failure",
"main pressure detection module",
"Short circuit protection failure",
"Low Voltage No Charging",
"RESERVED",
"RESERVED",
"RESERVED",
"RESERVED",
],
}

262
src/bmscore.js Normal file
View File

@@ -0,0 +1,262 @@
const { SerialPort } = require('serialport');
const errors = require('./errors');
const address = 0x04;
const frameLength = 13;
const timeoutMS = 700;
const retryDelayMS = 200;
//Arrotondamenti
const r1 = v => Math.round(v * 10) / 10; // 0.1 — tensione totale, corrente, SOC%
const r3 = v => Math.round(v * 1000) / 1000; // 0.001 — tensioni cella, capacità Ah
const r2 = v => Math.round(v * 100) / 100; // 0.01 — temperature in kelvin (SignalK)
class BMS {
constructor({ device, retries = 5, log = () => {} } = {}) {
this.device = device;
this.retries = retries;
this.log = log;
this.port = null;
this.status = null; // ultimo getStatus (n. celle, sensori temp)
this._queue = Promise.resolve(); // serializza le richieste sulla seriale
}
open() {
return new Promise((resolve, reject) => {
this.port = new SerialPort({
path: this.device,
baudRate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
autoOpen: false
});
this.port.open(err => err ? reject(err) : this.port.flush(() => resolve()));
});
}
async close() {
if (this.port && this.port.isOpen) {
await new Promise(r => this.port.close(() => r()));
}
this.port = null;
}
static _crc(buf) {
let s = 0;
for (let i = 0; i < buf.length; i++) s = (s + buf[i]) & 0xFF;
return s;
}
_buildFrame(cmd, extraHex = '') {
const hex = `a5${address.toString(16).padStart(2, '0')}${cmd}08${extraHex}`.padEnd(24, '0');
const buf = Buffer.from(hex, 'hex');
return Buffer.concat([buf, Buffer.from([BMS._crc(buf)])]);
}
//Invii multipli
_sendBatch(cmd, opts = {}) {
const run = async () => {
let lastErr;
for (let i = 0; i < this.retries; i++) {
try {
const r = await this._send(cmd, opts);
if (opts.returnList ? r && r.length : r) return r;
} catch (e) {
lastErr = e;
this.log(`cmd ${cmd} try ${i + 1} failed: ${e.message}`);
}
await new Promise(r => setTimeout(r, retryDelayMS));
}
throw lastErr || new Error(`cmd ${cmd} failed after ${this.retries} retries`);
};
const next = this._queue.then(run, run);
this._queue = next.catch(() => {}); // non rompere la coda al primo errore
return next;
}
_send(cmd, { extraHex = '', maxResponses = 1, returnList = false } = {}) {
return new Promise((resolve, reject) => {
if (!this.port || !this.port.isOpen) return reject(new Error('serial not open'));
const tx = this._buildFrame(cmd, extraHex);
const frames = [];
let buf = Buffer.alloc(0);
const cleanup = () => {
clearTimeout(timer);
this.port.off('data', onData);
};
const finish = () => {
cleanup();
if (returnList || frames.length > 1) return resolve(frames);
return resolve(frames[0] || null);
};
const onData = chunk => {
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
while (buf.length >= frameLength) {
const frame = buf.subarray(0, frameLength);
buf = buf.subarray(frameLength);
if (BMS._crc(frame.subarray(0, 12)) !== frame[12]) {
this.log(`crc mismatch: ${frame.toString('hex')}`);
continue;
}
if (frame[2].toString(16).padStart(2, '0') !== cmd) {
this.log(`unexpected cmd: ${frame.toString('hex')}`);
continue;
}
frames.push(frame.subarray(4, 12));
if (frames.length >= maxResponses) return finish();
}
};
const timer = setTimeout(() => {
if (frames.length === 0) { cleanup(); return reject(new Error('serial timeout')); }
finish();
}, timeoutMS);
this.port.flush(() => {
this.port.on('data', onData);
this.port.write(tx, err => { if (err) { cleanup(); reject(err); } });
});
});
}
// 0x90 — voltage / current / SOC
async getSoc() {
const d = await this._sendBatch('90');
if (!d) return null;
return {
total_voltage: r1(d.readInt16BE(0) / 10),
current: r1((d.readInt16BE(4) - 30000) / 10), // < 0 charging, > 0 discharging
soc_percent: r1(d.readInt16BE(6) / 10)
};
}
// 0x91 — range tensioni cella
async getCellVoltageRange() {
const d = await this._sendBatch('91');
if (!d) return null;
return {
highest_voltage: r3(d.readInt16BE(0) / 1000),
highest_cell: d.readInt8(2),
lowest_voltage: r3(d.readInt16BE(3) / 1000),
lowest_cell: d.readInt8(5)
};
}
// 0x92 — range temperature
async getTemperatureRange() {
const d = await this._sendBatch('92');
if (!d) return null;
return {
highest_temperature: d.readInt8(0) - 40,
highest_sensor: d.readInt8(1),
lowest_temperature: d.readInt8(2) - 40,
lowest_sensor: d.readInt8(3)
};
}
// 0x93 — MOSFET + capacità residua
async getMosfetStatus() {
const d = await this._sendBatch('93');
if (!d) return null;
const m = d.readInt8(0);
return {
mode: m === 0 ? 'stationary' : m === 1 ? 'charging' : 'discharging',
charging_mosfet: !!d[1],
discharging_mosfet: !!d[2],
capacity_ah: r3(d.readInt32BE(4) / 1000)
};
}
// 0x94 — status (n. celle, sensori, cicli, DI/DO)
async getStatus() {
const d = await this._sendBatch('94');
if (!d) return null;
const stateByte = d.readInt8(4);
const stateNames = ['DI1','DI2','DI3','DI4','DO1','DO2','DO3','DO4'];
const states = {};
for (let i = 0; i < 8; i++) states[stateNames[i]] = !!((stateByte >> i) & 1);
const data = {
cells: d.readInt8(0),
temperature_sensors: d.readInt8(1),
charger_running: !!d[2],
load_running: !!d[3],
states,
cycles: d.readInt16BE(5)
};
this.status = data;
return data;
}
async _ensureStatus() {
if (!this.status) await this.getStatus();
if (!this.status) throw new Error('cannot read BMS status');
return this.status;
}
_splitFrames(frames, total, perFrame, parser) {
const out = {};
let expected = 1;
for (const f of frames) {
const { idx, vals } = parser(f);
if (idx !== expected) { this.log(`frame out of order: exp ${expected} got ${idx}`); continue; }
for (let i = 0; i < perFrame; i++) {
const k = (expected - 1) * perFrame + i + 1;
if (k > total) return out;
out[k] = vals[i];
}
expected++;
}
return out;
}
// 0x95 — tensioni cella (3/frame)
async getCellVoltages() {
const st = await this._ensureStatus();
const max = Math.ceil(st.cells / 3);
const frames = await this._sendBatch('95', { maxResponses: max, returnList: true });
if (!frames) return null;
const cells = this._splitFrames(frames, st.cells, 3, f => ({
idx: f.readInt8(0),
vals: [f.readInt16BE(1), f.readInt16BE(3), f.readInt16BE(5)]
}));
for (const k of Object.keys(cells)) cells[k] = cells[k] / 1000;
return cells;
}
// 0x96 — temperature (7/frame, raw - 40 = °C)
async getTemperatures() {
const st = await this._ensureStatus();
const max = Math.ceil(st.temperature_sensors / 7);
const frames = await this._sendBatch('96', { maxResponses: max, returnList: true });
if (!frames) return null;
const t = this._splitFrames(frames, st.temperature_sensors, 7, f => ({
idx: f.readInt8(0),
vals: [f.readInt8(1), f.readInt8(2), f.readInt8(3), f.readInt8(4), f.readInt8(5), f.readInt8(6), f.readInt8(7)]
}));
for (const k of Object.keys(t)) t[k] = t[k] - 40;
return t;
}
// 0x98 — errori (bitmap)
async getErrors() {
const d = await this._sendBatch('98');
if (!d) return [];
const errors = [];
for (let i = 0; i < d.length; i++) {
const b = d[i];
if (!b) continue;
for (let bit = 0; bit < 8; bit++) {
if ((b >> bit) & 1) {
errors.push((errors[i] && errors[i][bit]) || `unknown(${i}.${bit})`);
}
}
}
return errors;
}
}
module.exports = BMS;

76
src/errors.js Normal file
View File

@@ -0,0 +1,76 @@
// Porting di dalybms/error_codes.py — indice[byte][bit] → stringa errore.
module.exports = [
// byte 0
[
'one stage warning of unit over voltage',
'one stage warning of unit over voltage',
'one stage warning of unit over voltage',
'two stage warning of unit over voltage',
'Total voltage is too high - level 1 alarm',
'Total voltage is too high - level 2 alarm',
'Total voltage is too low - level 1 alarm',
'Total voltage is too low - level 2 alarm'
],
// byte 1
[
'Charging temperature too high - level 1 alarm',
'Charging temperature too high - level 2 alarm',
'Charging temperature too low - level 1 alarm',
'Charging temperature too low - level 2 alarm',
'Discharge temperature too high - level 1 alarm',
'Discharge temperature too high - level 2 alarm',
'Discharge temperature too low - level 1 alarm',
'Discharge temperature too low - level 2 alarm'
],
// byte 2
[
'Charge over current - level 1 alarm',
'Charge over current - level 2 alarm',
'Discharge over current - level 1 alarm',
'Discharge over current - level 2 alarm',
'SOC too high - level 1 alarm',
'SOC too high - level 2 alarm',
'SOC too low - level 1 alarm',
'SOC too low - level 2 alarm'
],
// byte 3
[
'Excessive differential pressure - level 1 alarm',
'Excessive differential pressure - level 2 alarm',
'Excessive temperature difference - level 1 alarm',
'Excessive temperature difference - level 2 alarm',
'reserved', 'reserved', 'reserved', 'reserved'
],
// byte 4
[
'Charging MOS overtemperature warning',
'Discharge MOS overtemperature warning',
'Charging MOS temperature sensor failure',
'Discharge MOS temperature sensor failure',
'Charging MOS adhesion failure',
'Discharge MOS adhesion failure',
'Charging MOS breaker failure',
'Discharge MOS breaker failure'
],
// byte 5
[
'AFE acquisition chip malfunction',
'Monomer collect drop off',
'Single temperature sensor fault',
'EEPROM storage failure',
'RTC clock malfunction',
'Precharge failure',
'Vehicle communications error',
'Intranet communication module malfunction'
],
// byte 6
[
'Current module failure',
'Main pressure detection module failure',
'Short circuit protection failure',
'Low voltage no charging',
'reserved', 'reserved', 'reserved', 'reserved'
],
// byte 7
['reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved']
];