====== BMS: ======
The battery management system is a important part of the robot.
It is responsible to monitor all values regarding electrical power and available energy for the robot.
It also has protection features that ensure, that the battery used lasts as long as possible to make the system safe, reliable and sustainable.
Based on the measured values, the system can decide when to return to the home base for charging.
Also the important metrics can be sent to a control station to let the owner know about the status of the system.
This part of the project tries to implement a monitoring system for the important metrics of the power delivery system.
===== TinyBMS s516 =====
Product: [[https://www.energusps.com/shop/product/tiny-bms-s516-150a-750a-36]]
The TinyBMS s516 is a very capable and small battery management board with battery protection functions. It has many additional features like temperature and speed measurement and can be activated with an ignition/switch. It also has a precharge mode for an precharge circuit to avoid sparking or big power spikes when starting.
The main feature that makes this BMS a good choice is the ability to communicate with the chips via UART to access measured values.
User Manual: [[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21072]]
===== Power, balance and UART cables =====
Standard connection scheme: [[https://www.energusps.com/website/image/ir.attachment/10097_bde823f/datas]]
Balance wires starting highest cell at pin 16
Custom UART connection plug/cable, FTDI similar:\\
{{:projects:farmrobot:20201203_234347_2.jpg?nolink&200|}}
To connect to the UART port on the BMS a FST connector is needed with female pin headers on the other side to connect to the respective pins on the microcontroller / GPIO or FTDI adapter for a connection via USB to a PC.
The pins of the BMS are labled in the user manual.\\
The rx, tx and ground pins are connected between the BMS and the controller, where the tx cable from the BMS is attached to the specified rx pin of the microcontroller.
{{:projects:farmrobot:img_20201214_222504.jpg?nolink&200|}}
===== UART communication =====
UART Communication protocol: [[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21208]]
===== BatteryInsider PC Application =====
This application is provided alongside the BMS and can be downloaded at the product page:
[[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21067]]
Functions:
* Live Data
* Setting
* Statistics
* Cell Settings
* Logging data to files
* Maintenance
{{:projects:farmrobot:ir_attachment_1257.png?400|}}
https://www.energusps.com/website/image/ir.attachment/1257_da9a968/datas
The BMS firmware has been updated to Version 245, which has been send upon request by EnergusPS.
The firmware was flashed to the BMS board using the BatteryInsider Application connected via UART.
===== BMS configuration =====
Fully Charged Voltage: 4,00
Fully Discharged Voltage: 3,00
Early Balancing Threshold: 3,95
Charge Finished Current: 0,20
Battery Capacity: 30,0
Number of Series Cells: 6
Allowed Disbalance: 15
Set SOC manually, %: 95
Over-Voltage Cutoff: 4,20
Under-Voltage Cutoff: 2,80
Discharge Overcurrent Cutoff: 30
Charge Overcurrent Cutoff: 20
Over-Heat Cutoff: 40
Low Temp. Charger Disable: 0
Automatic Recovery: 1
BMS Mode: Dual Port
Single Port Switch Type: Internal FET
Load Switch Type: Discharge FET
Load Ignition: Disabled
Load Precharge: Disabled
Load Precharge Duration: 0.1 s
Charger Type: Generic CC/CV
Charger Switch Type: Charge FET
Charger Detection: Internal
Pulses Per Unit: 1
Distance Unit: Kilometers
Speed Sensor Input: Disabled
Broadcast: Disabled
Protocol: ASCII
Temperature Sensor Type: Dual 10K NTC Sensor
Invert External Current Sensor Direction: 0
Disable Load/Charger Switch Diagnostics: 0
===== Monitoring BMS =====
The UART interface is the only port that can make the internally gathered data of the BMS available to other devices.\\
But the monitoring device that is communicating with the BMS board is variable.\\
For easy and standard python implementation and further appliances, a Raspberry Pi 4B can be used, as it has GPIO and USB interfaces and can run the standard python scripts.\\
It could run as the standard network interface, making internet connections available to other systems or can host multiple appliances like camera live streaming, mqtt client and commander for the robot system which can send commands from remote to a device of the robot.\\
For more embedded and efficient devices, an Arduino or ESP32 can be used.
The ESP32 can run micropython code, which is a reduced feature set of python, which can also be extended with libaries, speciffic to micropython.\\
This is an additional hurdle, as the libaries are not identical to the standard python featureset.\\
For simplicity the appliances are developed on one plattform, the Raspberry Pi 4B.
===== Modbus Python and Watson-IoT MQTT publish on Raspberry Pi 4B =====
Using FTDI USB adapter which can be interfaced on /dev/ttyUSB0\\
Adding the current user to the "dialout" user group to access serial interfaces without root permissions:
sudo adduser pi dialout
Installing prerequisites:\\
sudo apt install python3
wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip install pymodbus\\
ModbusSerialClient is the Modbus client that is used to interface the registers on the BMS:
from pymodbus.client.sync import ModbusSerialClient
=== IBM Watson IoT platform ===
Creating a new device and gathering credentials: https://q74k3e.internetofthings.ibmcloud.com/dashboard/devices/browse/
Organization ID: q74k3e\\
Device Type: RaspberryPi4B\\
Device ID: farmrobot-raspi-4b-xi-lab-ip-15\\
Authentication Method: use-token-auth\\
Authentication Token: 5GNOxfx9&mUo7*?8fP\\
Watson IoT Python SDK documentation: https://ibm-watson-iot.github.io/iot-python/device/
pip install wiotp-sdk
Modbus communication implementation based on: https://github.com/clarkni5/tinybms/blob/master/python/tinybms.py\\
=== Code: ===
import numpy as np
import wiotp.sdk.device
from time import sleep
from datetime import datetime
import pymodbus.client.sync
dev_port = '/dev/ttyUSB0'
modbus_client = pymodbus.client.sync.ModbusSerialClient(method='rtu', port=dev_port, baudrate=115200, parity='N',
bytesize=8, stopbits=1, timeout=2, strict=False)
my_config = wiotp.sdk.device.parseConfigFile("device.yaml")
mqtt_client = wiotp.sdk.device.DeviceClient(config=my_config, logHandlers=None)
def connect_modbus():
if not modbus_client.is_socket_open():
modbus_client.connect()
print("connect_modbus: ok")
def connect_mqtt():
mqtt_client.connect()
print("connect_mqtt: ok")
def convert(array, da_type):
return np.array(array, dtype=np.uint16).view(dtype=da_type)[0]
def read_registers(address, count):
while True:
result = modbus_client.read_holding_registers(address, count, unit=0xAA)
if not result.isError():
register = result.registers
return register
def ask_registers():
read_registers(0, 1)
lifetime_counter = (convert(read_registers(32, 2), np.uint32))/60 # min
time_left = (convert(read_registers(34, 2), np.uint32))/60 # min
pack_voltage = convert(read_registers(36, 2), np.float32) # V
pack_current = convert(read_registers(38, 2), np.float32) # C
min_cell = (read_registers(40, 1)[0])/1000 # V
max_cell = (read_registers(41, 1)[0])/1000 # V
cell_diff = (read_registers(104, 1)[0])/10000 # V
soc = (convert(read_registers(46, 2), np.uint32))/1000000 # %
bms_temperature = (read_registers(48, 1)[0])/10 # °C
bms_online = hex(read_registers(50, 1)[0])
max_discharge_current = (read_registers(102, 1)[0])/1000 # A
max_charge_current = (read_registers(103, 1)[0])/1000 # A
charge_count = read_registers(111, 1)[0]
f_data = {
'lifetime_counter': "%.2f" % lifetime_counter,
'time_left': "%.2f" % time_left,
'pack_voltage': "%.2f" % pack_voltage,
'pack_current': "%.2f" % pack_current,
'min_cell': "%.2f" % min_cell,
'max_cell': "%.2f" % max_cell,
'cell_diff': "%.2f" % cell_diff,
'soc': "%.2f" % soc,
'bms_temperature': "%.1f" % bms_temperature,
'bms_online': str(bms_online),
'max_discharge_current': "%.2f" % max_discharge_current,
'max_charge_current': "%.2f" % max_charge_current,
'charge_count': str(charge_count)
}
print(f_data)
print("stored register values: ok")
return f_data
def publish(p_data):
mqtt_client.publishEvent(eventId="status", msgFormat="json", data=p_data, qos=0, onPublish=print("publish: ok"))
def output_time():
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print("-------------------------------------------------------------------")
print(current_time)
print("-------------------------------------------------------------------")
while True:
output_time()
connect_modbus()
connect_mqtt()
my_data = ask_registers()
publish(my_data)
sleep(10)
{{:projects:farmrobot:run_terminal.png|}}
==== Receiving MQTT messages on Watson IoT platform ====
Received messages:\\
{{:projects:farmrobot:watson_iot_received_messages.png?600|}}\\
Raw status data available:\\
{{:projects:farmrobot:watson_iot_raw_data.png?600|}}
===== Web Interface to view and graph the data =====
An easy way to set up a web interface is to host a node-red instance on a server, for example on a stationary Raspberry Pi 4, which can be accesses via network or can be made accessible with port forwarding from the internet.\\
Setting up a Raspberry Pi 4B with docker and docker run portainer.
Create a new Node-Red Stack with a compose file, which creates a node-red web instance on the device on port 1880.
The ip adress is needed which can be requested with:
ifconfig
version: "2"
services:
node-red:
image: nodered/node-red:latest
environment:
- TZ=Europe/Berlin
ports:
- "1880:1880"
networks:
- node-red-net
volumes:
- ~/data/node-red:/data
networks:
node-red-net:
The node-red webapp can then be accessed via [[http://ip-adress:1880]]\\
The Node-RED Dashboard module is needed to display the data with node-red\\
To install, click the Menu Button and choose "Manage palette". Click the "Install" tab and search for "node-red-dashboard".\\
Then click on install and install in the pop-up window. Then return to the main view.\\
{{:projects:farmrobot:node-red-palette.png?600|}}
===== Micropython implementation on espressiv ESP32 DevKitc v4 =====
Required software: Linux OS and python3 (sudo apt-get install python3)
Powering Esp32 via USB of computer
Downloading micropython firmware for microcontroller: https://micropython.org/download/esp32/
The modules that have an USB/UART chip like the espressiv ESP32 DevKitc v4 can be flashed and communicated with via the USB port.
Installing esptool for flashing to board
pip install esptool
Erasing existing firmware of microcontroller
esptool.py --port /dev/ttyUSB0 erase_flash
Flashing new micropython firmware
esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 esp32-idf4-20210202-v1.14.bin
All available commands of esptool can be seen when calling esptool.py -h
The REPL (Python prompt) is available on UART0 (which is connected to a USB-serial convertor, depending on the board) The baudrate is 115200.
picocom is a client to connect to a serial device.
sudo apt-get install picocom
picocom /dev/ttyUSB0 -b 115200 -f n -y n -d 8 -p 1
Connect to a WLAN network with internet access (smartphone hotspot) via the REPL and import and install upip (micro-pip / python package installer) and install umqtt packages.\\
>>> import network
>>> wlan = network.WLAN(network.STA_IF)
>>> wlan.active(True)
True
>>> wlan.connect('Jonas','test1234')
>>> import upip
>>> upip.install('micropython-umqtt.robust')
Installing to: /lib/
Warning: micropython.org SSL certificate is not validated
Installing micropython-umqtt.robust 1.0.1 from https://micropython.org/pi/umqtt.robust/umqtt.robust-1.0.1.tar.gz
>>> upip.install('micropython-umqtt.simple')
Installing to: /lib/
Installing micropython-umqtt.simple 1.3.4 from https://micropython.org/pi/umqtt.simple/umqtt.simple-1.3.4.tar.gz
from umqtt.robust import MQTTClient
IBM IoT cloud is an alternative also using mqtt, but there is a seperate client available.\\
1. Register with IBM Cloud\\
2. Create a Internet of Things Platform instance in Frankfurt using the free "lite" plan\\
Lite plan of IBM Watson IoT platform:
Includes up to 500 registered devices, and a maximum of 200 MB of each data metric
Maximum of 500 registered devices
Maximum of 500 application bindings
Maximum of 200 MB of each of data exchanged, data analyzed and edge data analyzed
3. Click “Create”\\
4. Click "Launch"\\
IBM IoT mqtt client: https://github.com/boneskull/micropython-watson-iot \\
import upip
upip.install('micropython-watson-iot')
https://q74k3e.internetofthings.ibmcloud.com/dashboard/devices/browse \\
"Add Device":\\
Organization ID: q74k3e\\
Device Type: ESP32\\
Device ID: farmrobot-bms-2021-47475\\
Authentication Method: use-token-auth\\
Authentication Token: ZrpD@cxveaiF?-WxE\\
Example:
from watson_iot import Device
d = Device(device_id='boneskull-esp32-test')
d.connect()
d.publishEvent('temperature', {'degrees': 68.5, 'unit': 'F'})
=== Connecting iot module ===
from watson_iot import Device
my_device = Device(
device_id='my-device-id', # required
device_type='my-device-type', # required
token='my-device-token', # required
# optional parameters shown with defaults below
org='quickstart',
username='use-token-auth',
port=8883, # this is 1883 if default `org` used
clean_session=True,
domain='internetofthings.ibmcloud.com',
ssl_params=None,
log_level='info'
)
my_device.connect()
=== Publishing an Event ===
Assuming the Device is connected, this example will publish a single event with ID my_event_id.
my_device.publishEvent(
'my_event_id', # event name
{'ok': True}, # message payload
message_format='json', # 'text' is also built-in
qos=0 # QoS 0 or QoS 1
)
=== Exploring files of esp32 ===
sudo pip3 install adafruit-ampy
ampy -p /dev/ttyUSB0 -b 115200
Pushing a file:
ampy -p /dev/ttyUSB0 -b 115200 put file(path)
Listing all files and directories in flash:
ampy -p /dev/ttyUSB0 -b 115200 ls
/boot.py
/main.py
/lib
Removing a file to replace it:
ampy -p /dev/ttyUSB0 -b 115200 rm main.py
ampy -p /dev/ttyUSB0 -b 115200 put /home/jonas/Documents/GitHub/farmrobot_jonas/bms/main.py
===== C++/Arduino implementation on espressiv ESP32 DevKit v4 =====
TX_Pin = 25 (orange), RX_Pin = 26 (gelb)\\
HardwareSerial BMS(1); //defining "BMS" as HardwareSerial on UART 1
byte rx_data[57];
byte bms_registers_msg[] = {0xAA, 0x09, 0x1A, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x04, 0x01, 0xA8, 0x3F};
byte init_seq[] = {0xAA, 0x09, 0x34};
float PackVoltage = 0;
float PackCurrent = 0;
float SystemPower = 0;
uint16_t MinCellV = 0; // (1 mV)
uint32_t SOC = 0; // (0.000001 %)
void setup() {
Serial.begin(115200);
BMS.begin(115200, SERIAL_8N1, 26, 25); //rx, tx
}
void loop()
{
askRegisters();
delay(100);
}
float bytesToFloat(byte byte_array[4])
{
float float_var;
memcpy(&float_var, byte_array, 4);
return float_var;
}
uint16_t bytesToUint16(byte byte_array[2])
{
uint16_t int_var;
memcpy(&int_var, byte_array, 2);
return int_var;
}
uint32_t bytesToUint32(byte byte_array[4])
{
uint32_t int_var;
memcpy(&int_var, byte_array, 4);
return int_var;
}
void askRegisters()
{
byte rx[500];
int j = 0;
BMS.write(bms_registers_msg, 31);
delay(10);
BMS.write(bms_registers_msg, 31);
while (BMS.available() > 0) { //read bytes when UART buffer is not empty
BMS.readBytes(rx, 500);
}
//finding start sequence
for (int i = 0; i < 500; i++) {
byte rx_current[] = {rx[i], rx[i + 1], rx[i + 2]};
if (memcmp(rx_current, init_seq, 3) == 0) { //comparing the memory content of the arrays to find the starting sequence of 0xAA, 0x09; 2 bytes long; memcmp returns 0 if it matches
/* check for buffer overflow Serial.print("found: break at i= ");Serial.println(i);*/ j = i; break;
}
}
for (int m = 0; m < 500; m++) {
Serial.println(rx[m],HEX);
}
int PL = (int)rx[j + 2];
Serial.print("PL = ");
Serial.println(PL);
for (int n = 0; n < 57; n++) {
rx_data[n] = (rx[j]);
Serial.print(rx_data[n], HEX);
Serial.print(",");
j++;
}
Serial.print("\n");
byte PackVoltage_array[4] = {rx_data[5], rx_data[6], rx_data[9], rx_data[10]};
PackVoltage = bytesToFloat(PackVoltage_array);
for (int k = 0; k < 4; k++) {
Serial.println(PackVoltage_array[k],HEX);
}
Serial.print("Batterypack Voltage: ");
Serial.print(PackVoltage);
Serial.println("V");
byte PackCurrent_array[4] = {rx_data[13], rx_data[14], rx_data[17], rx_data[18]};
PackCurrent = bytesToFloat(PackCurrent_array);
/*
for (int k = 0; k < 4; k++) {
Serial.println(PackCurrent_array[k],HEX);
}*/
Serial.print("Batterypack Current: ");
Serial.print(PackCurrent);
Serial.println("A");
SystemPower = PackVoltage * PackCurrent;
Serial.print("System Power: ");
Serial.print(SystemPower);
Serial.println("W");
byte MinCellV_array[2] = {rx_data[21], rx_data[22]};
MinCellV = bytesToUint16(MinCellV_array); // (1 mV) i+21, i+22 2 bytes
/*
for (int k = 0; k < 2; k++) {
Serial.println(MinCellV_array[k],HEX);
}*/
Serial.print("Minimal Cell Voltage: ");
Serial.print(MinCellV / 1000);
Serial.println("V");
byte SOC_array[4] = {rx_data[25], rx_data[26], rx_data[29], rx_data[30]};
SOC = bytesToUint32(SOC_array); // Resolution 0.000001 %
/*
for (int k = 0; k < 4; k++) {
Serial.println(SOC_array[k],HEX);
}*/
Serial.print("State of Charge: ");
Serial.print((SOC / 1000000));
Serial.println("%");
}
===== UART Python implementation on Raspberry Pi 4B =====
www.raspberrypi.org/documentation/configuration/uart.md
Pi 4 - 6 UARTS (UART1 is mini UART)
UART0 is secondary, which is used to connect bluetooth interface by default
first PL011 (UART0) is found as linux device under /dev/ttyAMA0
enable_uart=1 in /boot/firmware/config.txt enables PL011 (UART0) as primary interface
dtoverlay=disable-bt #restoring UART0/ttyAMA0 over GPIOs 14(tx) & 15(rx), making the full UART PL011 the primary interface /dev/serial0
sudo adduser pi dialout
enabling further uarts: www.raspberrypi.org/documentation/configuration/device-tree.md
sudo apt install python3
wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip install pyserial
backup:
sudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline-bp.txt
sudo nano /boot/firmware/cmdline.txt remove 'console=ttyAMA0,115200' and 'kgdboc=ttyAMA0,115200' if present
MQTT
ThingSpeak
https://de.mathworks.com/help/thingspeak/use-raspberry-pi-board-that-runs-python-websockets-to-publish-to-a-channel.html
sudo pip3 install paho-mqtt
sudo pip3 install psutil
import sys
import os
import serial
import time
import paho.mqtt.publish as publish
import psutil
import string
writeAPIKey = "V13QLD2JANKYVBV1"
mqttAPIKey = "R95SKCL1DYRQNF67"
channelID = "1314526"
mqttHost = "mqtt.thingspeak.com"
mqttUsername = "JonasGessmann"
tTransport = "websockets"
tPort = 80
topic = "channels/" + channelID + "/publish/" + writeAPIKey
ask_etl = b'\xAA\x09\x04\x22\x00\x23\x00\xF3\x1B'#ask_etl = bytes([0xAA, 0x09, 0x04, 0x22, 0x00, 0x23, 0x00, 0xF3, 0x1B]) #reg:34,35; [UINT_32] / Resolution 1 s R
ask_packv = bytes([0xAA, 0x09, 0x04, 0x24, 0x00, 0x25, 0x00, 0xF0, 0x33]) #reg:36,37; [FLOAT] / Resolution 1 V R
ask_packc = bytes([0xAA, 0x09, 0x04, 0x26, 0x00, 0x27, 0x00, 0xF0, 0xEB]) #reg:38,39; [FLOAT] / Resolution 1 A R
#syspow [FLOAT] / Resolution 1 W R
ask_mincv = bytes([0xAA, 0x09, 0x02, 0x28, 0x00, 0x60, 0x45]) #reg:40; [UINT_16] / Resolution 1 mV R
ask_maxcv = bytes([0xAA, 0x09, 0x02, 0x29, 0x00, 0x81, 0xD4]) #reg:41; [UINT_16] / Resolution 1 mV R
#inbalance of cells (maxcv - mincv
ask_soc = bytes([0xAA, 0x09, 0x04, 0x2E, 0x00, 0x2F, 0x00, 0xF5, 0x4B]) #reg:46,47; [UINT_32] / Resolution 0.000001 % R
ask_bmstemp = bytes([0xAA, 0x09, 0x02, 0x30, 0x00, 0x8A, 0x44])#reg:48; [INT_16] / Resolution 0.1 °C R
ask_bmsstatus = bytes([0xAA, 0x09, 0x02, 0x32, 0x00, 0x8B, 0x24]) #reg:50 BMS Online Status [UINT_16] / 0x91-Charging, 0x92-Fully Charged, 0x93-Discharging, 0x96-Regenertion, 0x97-Idle, 0x9B-Fault R
ask_nevents = bytes([0xAA, 0x11, 0xBF, 0x1C]) #Read Tiny BMS newest Events
init_seq_2 = b'\xAA\x09\x04'
init_seq_4 = b'\xAA\x09\x08'
int etl #convert seconds to minutes?
float packv
float packc
float syspow #SystemPower = PackVoltage * PackCurrent
float mincv #MinCellV/1000
#float maxcv
#float cvinbal
int soc #SOC/1000000
float bmstemp
#str bmsstatus
#str events
BMS = serial.Serial(port='/dev/ttyAMA0',baudrate=115200,bytesize=serial.EIGHTBITS,parity=serial.PARITY_NONE,stopbits=serial.STOPBITS_ONE, timeout=1,xonxoff=0,rtscts=0,dsrdtr=0,write_timeout=1,)
while 1:
etl = int(read_status_4(ask_etl))
payload = "etl=" + str(etl) + "&packv=" + str(packv) + "&packc=" + str(packc) + "&syspow=" + str(syspow) + "&mincv=" + str(mincv) + "&soc=" + str(soc) + "&bmstemp=" + str(bmstemp) """+ "&bmsstatus=" + str(bmsstatus""")
try:
publish.single(topic, payload, hostname=mqttHost, transport=tTransport, port=tPort,auth={'username':mqttUsername,'password':mqttAPIKey})
def read_status_2(ask_seq) {
bms_dump = bytes([])
BMS.reset_input_buffer()
BMS.reset_output_buffer()
BMS.write(ask_seq)
BMS.flush()
BMS.write(ask_seq)
BMS.flush()
bms_dump = BMS.read_until(init_seq_2)
msg = BMS.read(size=6)
return msg
}
def read_status_4(ask_seq) {
bms_dump = bytes([])
msg = bytes ([])
pl_len = 8
BMS.reset_input_buffer()
BMS.reset_output_buffer()
BMS.write(ask_seq)
BMS.flush()
BMS.write(ask_seq)
BMS.flush()
bms_dump = BMS.read_until(init_seq_4)
msg = BMS.read(size=(pl_len+5))
return msg
}
"""send ask_sequence twice
receive answer sequence and store it
check init Sequence (eg AA 09 04)
calculate and check CRC16 MODBUS checkbit
read and encode value to variable"""
===== Authors =====
* Jonas Geßmann, Environment & Energy, jonas.gessmann@protonmail.com\\