06-03-2024, 12:28 AM
these BIN file firmware and arduino source code work for KinCony KC868-Hx series relay board:
KC868-H32, KC868-H32L, KC868-H32W, KC868-H32BS, KC868-H32B
KC868-H16, KC868-H16W, KC868-H16B
KC868-H8, KC868-H8W, KC868-H8B
you not need connect any cable between Tuya adapter v2 and relay board. Because of it use by TCP connection, it use network.
fimrware download:
[attachment=5336]
[attachment=5337]
[attachment=5338]
download file and unzip, then download BIN file by ESP DOWNLOAD TOOL, write code at 0x0 address.
[attachment=5329]
ESP DOWNLOAD TOOL download link:
https://www.kincony.com/wp-content/uploa..._3.9.2.zip
if you want modify arduino source code, which relay board model to use, just uncomment the model command line.
first time run code, you need to set relay board's IP address, port, your router's ssid and password for Tuya adapter v2 by usb-serial port.
just send command as this: SSID:KinCony,PSW:a12345678,192.168.3.141,4196
serial port baud rate: 115200bps
serial port debug tool download:
[attachment=5335]
here is arduino source code:
here is arduino source code download link:
[attachment=5330]
KC868-H32, KC868-H32L, KC868-H32W, KC868-H32BS, KC868-H32B
KC868-H16, KC868-H16W, KC868-H16B
KC868-H8, KC868-H8W, KC868-H8B
you not need connect any cable between Tuya adapter v2 and relay board. Because of it use by TCP connection, it use network.
fimrware download:
[attachment=5336]
[attachment=5337]
[attachment=5338]
download file and unzip, then download BIN file by ESP DOWNLOAD TOOL, write code at 0x0 address.
[attachment=5329]
ESP DOWNLOAD TOOL download link:
https://www.kincony.com/wp-content/uploa..._3.9.2.zip
if you want modify arduino source code, which relay board model to use, just uncomment the model command line.
first time run code, you need to set relay board's IP address, port, your router's ssid and password for Tuya adapter v2 by usb-serial port.
just send command as this: SSID:KinCony,PSW:a12345678,192.168.3.141,4196
serial port baud rate: 115200bps
serial port debug tool download:
[attachment=5335]
here is arduino source code:
Code:
// USB set command SSID:XXXXX,PSW:XXXXX,X.X.X.X,YYYY
// X.X.X.X is relay board IP address, YYYY is relay board port. SSID and PSW is your router's ssid and password
// for example SSID:KinCony,PSW:a12345678,192.168.3.141,4196
// make sure your H8,H16,H32 relay board work with "TCP Server" mode.
#include <Arduino.h>
#include <TuyaWifi.h>
#include <Preferences.h>
#include "HardwareSerial.h"
///ADD TCP CLIENT FOR TUYA ADAPT
//#include <WiFiUdp.h>
#include <WiFi.h>
#include <WiFiClient.h>
//uncomment the relay board model you are using
//#define KINCONY_H8X ////
//#define KINCONY_H16X ////
#define KINCONY_H32X ////
#if defined(KINCONY_H8X)
#define RLY_NUMS 8
#elif defined(KINCONY_H16X)
#define RLY_NUMS 16
#elif defined(KINCONY_H32X)
#define RLY_NUMS 32
#endif
//---------RS485 setting
#define DEBUG 1
#define SERIAL_BAUD 9600
#define RX_PIN 32
#define TX_PIN 33
#define ADAPT485_NUM 2
#define SLAVE_ADDR 1
#define READ_ADDR 0
#define READ_NUM 32
////SERIAL COMMAND IS
////SSID,
char ssid_buf[30] = {0};
char password_buf[20] = {0};
char server_ip[20] = {0};
uint16_t server_port = 4096;
uint8_t str[]={"client_me"};
//uint8_t Rece[256]={0};
uint8_t Send[128]={0};
Preferences prefs;
//IPAddress staticIP(192, 168, 10, 12);
//IPAddress gateway(192, 168, 10, 1);
//IPAddress subnet(255, 255, 255, 0);
//IPAddress dns(192, 168, 10,1);
WiFiClient client;
typedef enum{INIT_STATUS=0,RDY_STATUS,CONNECT_STATUS,TCP_COMM,STOP_STATUS,}STATUS_MACHINE;
static int statusMachine = INIT_STATUS;
static void InitWifi(void)
{
//WiFi.mode(WIFI_STA);
//WiFi.setSleep(false); // disable STA mode wifi sleep mode
/*if(WiFi.config(staticIP, gateway, subnet, dns, dns)==false)
{
Serial1.println("Configuration failed.");
}*/
/*IPAddress ip = WiFi.localIP();
Serial1.print("IP address: ");
Serial1.println(WiFi.localIP()); */
Serial1.print("rcv SSID is: ");
Serial1.println(ssid_buf);
Serial1.print("rcv PSW is: ");
Serial1.println(password_buf);
Serial1.print("rcv server ip is: ");
Serial1.println(server_ip);
Serial1.print("rcv server port is: ");
Serial1.println(server_port);
WiFi.begin(ssid_buf, password_buf);//link wifi hotspot
Serial1.println("WiFi try to connect~");
statusMachine = RDY_STATUS;
}
static bool bFlagConnWifi = false;
static bool bFlagConnTcp = false;
static String serial_cmd_rcv = "";
static String tcp_cmd_rcv = "";
static char command_str_arr[30]={0};
bool rt_coils_status[RLY_NUMS] = {0};
void dp_update_all(void);
////online check for wifi&tcp connections for 500ms
static void SetRlyByChannel(uint8_t ch,bool value)
{
memset(command_str_arr,0,sizeof(command_str_arr));
sprintf(command_str_arr,"RELAY-SET-255,%d,%d",ch+1,value?1:0);
return;
}
static void ReadAllRlyStatus()
{
memset(command_str_arr,0,sizeof(command_str_arr));
memcpy(command_str_arr,(char*)"RELAY-STATE-255",15);
return;
}
static void WifiConnect(void)
{
if (WiFi.status() != WL_CONNECTED) //wait wifi connection success
{
Serial1.print(".");
//WiFi.begin(ssid_buf, password_buf);//link wifi hotspot
//Serial1.println("WiFi try to connect~");
bFlagConnWifi = bFlagConnTcp = false;
return ;
}
bFlagConnWifi = true;
statusMachine = CONNECT_STATUS;
Serial1.println("WiFi connected");
}
static void TcpServerConn(void)
{
if(WiFi.status() != WL_CONNECTED)
{
if(bFlagConnTcp) client.stop();
bFlagConnWifi = bFlagConnTcp = false;
statusMachine = RDY_STATUS;
Serial1.println("WiFi exception 0");
return;
}
if (!bFlagConnTcp)//begin connect to tcp server///if (client.connected() != 0)
{
if (!client.connect(server_ip, server_port,2000))
{
Serial1.println("try to connect host....");
bFlagConnTcp = false;
//delay(5);
}
else
{
bFlagConnTcp = true;
statusMachine = TCP_COMM;
Serial1.println("TCP connected Succeed");
}
}
}
static void SendPeriodCheckCommand(void)
{
if(WiFi.status() != WL_CONNECTED)
{
if(bFlagConnTcp) client.stop();
bFlagConnWifi = bFlagConnTcp = false;
statusMachine = RDY_STATUS;
Serial1.println("WiFi exception 1");
return;
}
if(client.connected())
Serial1.println("send period check cmd~");
statusMachine = TCP_COMM;
ReadAllRlyStatus();
//SetRlyByChannel();
client.write(command_str_arr,strlen(command_str_arr));
}
static void GetCoilStatus(String data)
{///ACK STANDARD FORMAT IS "RELAY-STATE-255,D1,D0,OK" ///16x
////ACK STANDARD FORMAT IS "RELAY-STATE-255,D0,OK" ///8x
////ACK STANDARD FORMAT IS "RELAY-STATE-255,D3,D2,D1,D0,OK" ///32x
String rcv_data = data;
if(rcv_data.substring(15,16) != "," ) return;
uint8_t rlys_arr[4] = {0};
uint32_t rly_status = 0;
rcv_data = rcv_data.substring(16);
//Serial1.println("Remain strs1:" + rcv_data);
int index_find = rcv_data.indexOf(',');
rlys_arr[0] = atoi(rcv_data.substring(0,index_find).c_str());
#if defined(KINCONY_H16X) || defined(KINCONY_H32X)
rcv_data = rcv_data.substring(index_find+1);
//Serial1.println("Remain strs2:" + rcv_data);
index_find = rcv_data.indexOf(',');
rlys_arr[1] = atoi(rcv_data.substring(0,index_find).c_str());
#endif
#if defined(KINCONY_H32X)
rcv_data = rcv_data.substring(index_find+1);
index_find = rcv_data.indexOf(',');
rlys_arr[2] = atoi(rcv_data.substring(0,index_find).c_str());
rcv_data = rcv_data.substring(index_find+1);
index_find = rcv_data.indexOf(',');
rlys_arr[3] = atoi(rcv_data.substring(0,index_find).c_str());
#endif
#if defined(KINCONY_H8X)
rly_status = rlys_arr[0];
#elif defined(KINCONY_H16X)
rly_status = (rlys_arr[0]<<8 ) | rlys_arr[1];
#else
rly_status = (rlys_arr[3]<<24 ) | (rlys_arr[2]<<16 )| (rlys_arr[1]<<8 )| (rlys_arr[0] );
#endif
Serial1.print("rly_status = 0x");
Serial1.println(rly_status,HEX);
for(int i=0;i<RLY_NUMS;i++)
{
rt_coils_status[i] = (rly_status&(1<<i))?true:false;
}
}
static char rece_tcp_buf[100] = {0};
static bool CheckClientRcv(void)
{
uint8_t i=0;
if(client.available() == 0) return false;
memset(rece_tcp_buf,0,sizeof(rece_tcp_buf));
tcp_cmd_rcv = "";
while (client.available()>0) //whether received data from server
{
rece_tcp_buf[i++] = client.read();//receive data from server
//tcp_cmd_rcv += client.read();//store data from server
delay(1);
}
tcp_cmd_rcv = String(rece_tcp_buf);
Serial1.print("Ack:");
Serial1.println(tcp_cmd_rcv.c_str());
if (tcp_cmd_rcv.length() < 22 ||
tcp_cmd_rcv.substring(0,15) != "RELAY-STATE-255" ) {tcp_cmd_rcv="";return false;}
//Serial1.println("OKAY COILS VALUE");
GetCoilStatus(tcp_cmd_rcv);
tcp_cmd_rcv="";
return true;
}
static bool ProtocolDeal(const char* buf)
{
char deal_str[100] = {0};
char tmp[30];
char *pos,*pos1,*pos2,*pos3;
memcpy(deal_str,buf,strlen(buf));
Serial1.println("check1");
if ((pos = strchr(deal_str,','))!=0) //find','
{
*pos = '\0'; //","replace with ‘\0’
pos++;
pos1 = pos;
memset(tmp,0,30);
//memcpy(tmp,buf,strlen(buf));
strcpy(tmp,deal_str);
if(tmp[0]=='S' && tmp[1]=='S' && tmp[2]=='I' && tmp[3]=='D' &&
tmp[4]==':')
{
memset(ssid_buf,0,sizeof(ssid_buf));
memcpy(ssid_buf,&tmp[5],strlen(tmp)-5);
Serial1.println("rcv ssid ok.");
}
}
else
{
Serial1.println("end1");
return false;
}
Serial1.println("check2");
if ((pos = strchr(pos1,','))!=0) //find','
{
*pos = '\0'; //","replace with‘\0’
pos++;
pos2 = pos;
memset(tmp,0,30);
//memcpy(tmp,pos1,strlen(pos1));
strcpy(tmp,pos1);
if(tmp[0]=='P' && tmp[1]=='S' && tmp[2]=='W' && tmp[3]==':' )
{
memset(password_buf,0,sizeof(password_buf));
memcpy(password_buf,&tmp[4],strlen(tmp)-4);
Serial1.println("rcv psw ok.");
}
}
else{
return false;
Serial1.println("end2");}
Serial1.println("check3");
if ((pos = strchr(pos2,','))!=0) //find',' IP:XXXX,PORT:XXXX
{
*pos = '\0'; //","replace with‘\0’
pos++;
pos3 = pos;
memset(server_ip,0,sizeof(server_ip));
memcpy(server_ip,pos2,strlen(pos2));
server_port = atoi(pos3);
Serial1.print("rcv SSID is: ");
Serial1.println(ssid_buf);
Serial1.print("rcv PSW is: ");
Serial1.println(password_buf);
Serial1.print("rcv server ip is: ");
Serial1.println(server_ip);
Serial1.print("rcv server port is: ");
Serial1.println(server_port);
/*bFlagConnWifi = bFlagConnTcp = false;
statusMachine = INIT_STATUS;
if(client.connected())
{
client.stop();
}
if (WiFi.status() == WL_CONNECTED) //等待WiFi热点连接成功
{
Serial1.print("stop wifi second.");
WiFi.disconnect();
delay(3000);
}*/
///Preferences param here...
prefs.begin("myns_netcfg");
prefs.putString("ns_ssid",ssid_buf);
prefs.putString("ns_psw",password_buf);
prefs.putString("ns_ip",server_ip);
prefs.putUShort("ns_port",server_port);
prefs.end();
ESP.restart();
}
else
{
return false;
Serial1.println("end3");
}
}
static void GetSerialCmd()
{ ////format: SSID:REALTIME2SENS-2.4G,PSW:12345678,IP:192.168.1.100,PORT:4196
if(Serial1.available() == 0) return;
serial_cmd_rcv = "";
while (Serial1.available() > 0)
{
serial_cmd_rcv += char(Serial1.read());
delay(1);
}
Serial1.println(serial_cmd_rcv);
if (serial_cmd_rcv.length() < 36 ||
serial_cmd_rcv.substring(0,4) != "SSID" ) {serial_cmd_rcv="";return;}
ProtocolDeal(serial_cmd_rcv.c_str());
serial_cmd_rcv = "";
}
/* Current device DP values */
HardwareSerial My485Serial(ADAPT485_NUM);
//---RS485 setting end----
TuyaWifi my_device;
/* Current LED status */
unsigned char led_state = 0;
/* Connect network button pin */
int key_pin = 25;
/* Data point define */
typedef enum
{
DPID_SWITCH01 = 1,
DPID_SWITCH02,DPID_SWITCH03,
DPID_SWITCH04,DPID_SWITCH05,DPID_SWITCH06,
DPID_SWITCH07 = 101,
DPID_SWITCH08,DPID_SWITCH09,DPID_SWITCH10,
DPID_SWITCH11,DPID_SWITCH12,DPID_SWITCH13,
DPID_SWITCH14,DPID_SWITCH15,DPID_SWITCH16,
DPID_SWITCH17,DPID_SWITCH18,DPID_SWITCH19,
DPID_SWITCH20,DPID_SWITCH21,DPID_SWITCH22,
DPID_SWITCH23,DPID_SWITCH24,DPID_SWITCH25,
DPID_SWITCH26,DPID_SWITCH27,DPID_SWITCH28,
DPID_SWITCH29,DPID_SWITCH30,DPID_SWITCH31,
DPID_SWITCH32,
}HXBS_DPID_DEF;
#define LED_WiFi 26
//#define LED_User 33
/* Stores all DPs and their types. PS: array[][0]:dpid, array[][1]:dp type.
* dp type(TuyaDefs.h) : DP_TYPE_RAW, DP_TYPE_BOOL, DP_TYPE_VALUE, DP_TYPE_String, DP_TYPE_ENUM, DP_TYPE_BITMAP
*/
#if defined(KINCONY_H16X)
unsigned char dp_array[][2] =
{
{DPID_SWITCH01, DP_TYPE_BOOL},{DPID_SWITCH02, DP_TYPE_BOOL},
{DPID_SWITCH03, DP_TYPE_BOOL},{DPID_SWITCH04, DP_TYPE_BOOL},
{DPID_SWITCH05, DP_TYPE_BOOL},{DPID_SWITCH06, DP_TYPE_BOOL},
{DPID_SWITCH07, DP_TYPE_BOOL},{DPID_SWITCH08, DP_TYPE_BOOL},
{DPID_SWITCH09, DP_TYPE_BOOL},{DPID_SWITCH10, DP_TYPE_BOOL},
{DPID_SWITCH11, DP_TYPE_BOOL},{DPID_SWITCH12, DP_TYPE_BOOL},
{DPID_SWITCH13, DP_TYPE_BOOL},{DPID_SWITCH14, DP_TYPE_BOOL},
{DPID_SWITCH15, DP_TYPE_BOOL},{DPID_SWITCH16, DP_TYPE_BOOL},
};
unsigned char pid[] = {"7en1hzfnjtvprt35"}; ////change pid here....
#elif defined(KINCONY_H8X)
unsigned char dp_array[][2] =
{
{DPID_SWITCH01, DP_TYPE_BOOL},{DPID_SWITCH02, DP_TYPE_BOOL},
{DPID_SWITCH03, DP_TYPE_BOOL},{DPID_SWITCH04, DP_TYPE_BOOL},
{DPID_SWITCH05, DP_TYPE_BOOL},{DPID_SWITCH06, DP_TYPE_BOOL},
{DPID_SWITCH07, DP_TYPE_BOOL},{DPID_SWITCH08, DP_TYPE_BOOL},
};
unsigned char pid[] = {"zyauvyer5insa2ne"}; ////change pid here....
#elif defined(KINCONY_H32X)
unsigned char dp_array[][2] =
{
{DPID_SWITCH01, DP_TYPE_BOOL},{DPID_SWITCH02, DP_TYPE_BOOL},
{DPID_SWITCH03, DP_TYPE_BOOL},{DPID_SWITCH04, DP_TYPE_BOOL},
{DPID_SWITCH05, DP_TYPE_BOOL},{DPID_SWITCH06, DP_TYPE_BOOL},
{DPID_SWITCH07, DP_TYPE_BOOL},{DPID_SWITCH08, DP_TYPE_BOOL},
{DPID_SWITCH09, DP_TYPE_BOOL},{DPID_SWITCH10, DP_TYPE_BOOL},
{DPID_SWITCH11, DP_TYPE_BOOL},{DPID_SWITCH12, DP_TYPE_BOOL},
{DPID_SWITCH13, DP_TYPE_BOOL},{DPID_SWITCH14, DP_TYPE_BOOL},
{DPID_SWITCH15, DP_TYPE_BOOL},{DPID_SWITCH16, DP_TYPE_BOOL},
{DPID_SWITCH17, DP_TYPE_BOOL},{DPID_SWITCH18, DP_TYPE_BOOL},
{DPID_SWITCH19, DP_TYPE_BOOL},{DPID_SWITCH20, DP_TYPE_BOOL},
{DPID_SWITCH21, DP_TYPE_BOOL},{DPID_SWITCH22, DP_TYPE_BOOL},
{DPID_SWITCH23, DP_TYPE_BOOL},{DPID_SWITCH24, DP_TYPE_BOOL},
{DPID_SWITCH25, DP_TYPE_BOOL},{DPID_SWITCH26, DP_TYPE_BOOL},
{DPID_SWITCH27, DP_TYPE_BOOL},{DPID_SWITCH28, DP_TYPE_BOOL},
{DPID_SWITCH29, DP_TYPE_BOOL},{DPID_SWITCH30, DP_TYPE_BOOL},
{DPID_SWITCH31, DP_TYPE_BOOL},{DPID_SWITCH32, DP_TYPE_BOOL},
};
unsigned char pid[] = {"ytgvxhy4zoaxdqtp"}; ////change pid here....
#endif
unsigned char mcu_ver[] = {"1.0.0"};
/* last time */
unsigned long last_time = 0;
/**
* @description: DP download callback function.
* @param {unsigned char} dpid
* @param {const unsigned char} value
* @param {unsigned short} length
* @return {unsigned char}
*/
unsigned char dp_process(unsigned char dpid,const unsigned char value[], unsigned short length)
{
int16_t index_out_coil = -1;
unsigned char get_down_cmd = false;
for(int16_t i=0;i<RLY_NUMS;i++)
{
if(dpid == dp_array[i][0])
{
index_out_coil = i;
break;
}
}
#if DEBUG
Serial1.print("Rcv Command:<dpid,");
Serial1.print(dpid,DEC);
Serial1.print(" value,");
Serial1.print(value[0],DEC);
Serial1.print(" len,");
Serial1.print(length,DEC);
Serial1.println("> ");
#endif
if(index_out_coil < 0) return TY_ERROR;
get_down_cmd = my_device.mcu_get_dp_download_data(dpid,value,length);
#if DEBUG
Serial1.print("get_down_cmd value is:<");
Serial1.print(get_down_cmd,DEC);
Serial1.println("> ");
#endif
SetRlyByChannel(index_out_coil,get_down_cmd?true:false);
Serial1.print("Send board control command:");
Serial1.println(command_str_arr);
uint16_t result = client.write(command_str_arr,strlen(command_str_arr));
#if DEBUG
Serial1.print("client WriteCoilAddr<");
Serial1.print(index_out_coil,DEC);
Serial1.print("> result:<");
Serial1.print(result,DEC);
Serial1.println("> ");
#endif
my_device.mcu_dp_update(dpid, value, length);
return TY_SUCCESS;
}
void setup()
{
My485Serial.begin(SERIAL_BAUD, SERIAL_8N1, RX_PIN, TX_PIN); //RS485 serial port
Serial.begin(9600,SERIAL_8N1,22,19); //Tuya module serial port
Serial1.begin(115200,SERIAL_8N1,3,1); //USB serial port
// Serial with tuyawifi
//Initialize led port, turn off led.
pinMode(LED_WiFi, OUTPUT);
digitalWrite(LED_WiFi, LOW);
/* pinMode(LED_User, OUTPUT);
digitalWrite(LED_User, LOW);*/
//Initialize networking keys.
pinMode(key_pin, INPUT_PULLUP);
//Enter the PID and MCU software version
my_device.init(pid, mcu_ver);
//incoming all DPs and their types array, DP numbers
my_device.set_dp_cmd_total(dp_array, 2);
//register DP download processing callback function
my_device.dp_process_func_register(dp_process);
//register upload all DP callback function
my_device.dp_update_all_func_register(dp_update_all);
//delay(300);
last_time = millis();
///Preferences param here...
prefs.begin("myns_netcfg");
String ns_ssid = prefs.getString("ns_ssid","123");
String ns_psw = prefs.getString("ns_psw","123");
String ns_ip = prefs.getString("ns_ip","127.0.0.1");
server_port = prefs.getUShort("ns_port",4096);
memcpy(ssid_buf,ns_ssid.c_str(),ns_ssid.length());
memcpy(password_buf,ns_psw.c_str(),ns_psw.length());
memcpy(server_ip,ns_ip.c_str(),ns_ip.length());
prefs.end();
WiFi.mode(WIFI_STA); //set to STA mode
}
static void FuncStatusMachine(int status)
{
switch(status)
{
case INIT_STATUS:
InitWifi();break;
case RDY_STATUS:
WifiConnect();break;
case CONNECT_STATUS:
TcpServerConn();break;
case TCP_COMM:
SendPeriodCheckCommand();break;
case STOP_STATUS:
break;
default:break;
}
}
void loop()
{
FuncStatusMachine(statusMachine); // read RS485 sensor
delay(100);
CheckClientRcv();
my_device.uart_service();
delay(100);
GetSerialCmd();
//Enter the connection network mode when Pin7 is pressed.
if (digitalRead(key_pin) == LOW) {
delay(80);
if (digitalRead(key_pin) == LOW) {
my_device.mcu_set_wifi_mode(SMART_CONFIG);
}
}
/* LED blinks when network is being connected */
if ((my_device.mcu_get_wifi_work_state() != WIFI_LOW_POWER) && (my_device.mcu_get_wifi_work_state() != WIFI_CONN_CLOUD) && (my_device.mcu_get_wifi_work_state() != WIFI_SATE_UNKNOW)) {
if (millis()- last_time >= 500) {
last_time = millis();
if (led_state == LOW) {
led_state = HIGH;
} else {
led_state = LOW;
}
digitalWrite(LED_WiFi, led_state);
}
}
/* report the temperature and humidity */
if ((my_device.mcu_get_wifi_work_state() == WIFI_CONNECTED) ||
(my_device.mcu_get_wifi_work_state() == WIFI_CONN_CLOUD)) {
dp_update_all();
}
delay(800);
}
static bool coils_old_status[RLY_NUMS] = {0};
static void CompareAndReport(void)
{
for(int16_t i=0;i<RLY_NUMS;i++)
{
if(coils_old_status[i] != rt_coils_status[i])
{
coils_old_status[i] = rt_coils_status[i];
my_device.mcu_dp_update(dp_array[i][0],rt_coils_status[i], 1);
break;
}
}
}
/**
* @description: Upload all DP status of the current device.
* @param {*}
* @return {*}
*/
void dp_update_all(void)
{
CompareAndReport();
}
[attachment=5330]