Merge branch 'master' into circleci-project-setup
This commit is contained in:
commit
e8890a879c
|
@ -57,7 +57,7 @@ GwChannel::GwChannel(GwLog *logger,
|
||||||
this->logger = logger;
|
this->logger = logger;
|
||||||
this->name=name;
|
this->name=name;
|
||||||
this->sourceId=sourceId;
|
this->sourceId=sourceId;
|
||||||
this->maxSourceId=sourceId;
|
this->maxSourceId=maxSourceId;
|
||||||
this->countIn=new GwCounter<String>(String("count")+name+String("in"));
|
this->countIn=new GwCounter<String>(String("count")+name+String("in"));
|
||||||
this->countOut=new GwCounter<String>(String("count")+name+String("out"));
|
this->countOut=new GwCounter<String>(String("count")+name+String("out"));
|
||||||
this->impl=NULL;
|
this->impl=NULL;
|
||||||
|
@ -146,12 +146,15 @@ bool GwChannel::canReceive(const char *buffer){
|
||||||
}
|
}
|
||||||
|
|
||||||
int GwChannel::getJsonSize(){
|
int GwChannel::getJsonSize(){
|
||||||
int rt=2;
|
int rt=JSON_OBJECT_SIZE(6);
|
||||||
if (countIn) rt+=countIn->getJsonSize();
|
if (countIn) rt+=countIn->getJsonSize();
|
||||||
if (countOut) rt+=countOut->getJsonSize();
|
if (countOut) rt+=countOut->getJsonSize();
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
void GwChannel::toJson(GwJsonDocument &doc){
|
void GwChannel::toJson(GwJsonDocument &doc){
|
||||||
|
JsonObject jo=doc.createNestedObject("ch"+name);
|
||||||
|
jo["id"]=sourceId;
|
||||||
|
jo["max"]=maxSourceId;
|
||||||
if (countOut) countOut->toJson(doc);
|
if (countOut) countOut->toJson(doc);
|
||||||
if (countIn) countIn->toJson(doc);
|
if (countIn) countIn->toJson(doc);
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,128 @@ void GwChannelList::allChannels(ChannelAction action){
|
||||||
action(*it);
|
action(*it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
typedef struct {
|
||||||
|
int id;
|
||||||
|
const char *baud;
|
||||||
|
const char *receive;
|
||||||
|
const char *send;
|
||||||
|
const char *direction;
|
||||||
|
const char *toN2K;
|
||||||
|
const char *readF;
|
||||||
|
const char *writeF;
|
||||||
|
const char *name;
|
||||||
|
} SerialParam;
|
||||||
|
|
||||||
|
static SerialParam serialParameters[]={
|
||||||
|
{
|
||||||
|
.id=SERIAL1_CHANNEL_ID,
|
||||||
|
.baud=GwConfigDefinitions::serialBaud,
|
||||||
|
.receive=GwConfigDefinitions::receiveSerial,
|
||||||
|
.send=GwConfigDefinitions::sendSerial,
|
||||||
|
.direction=GwConfigDefinitions::serialDirection,
|
||||||
|
.toN2K=GwConfigDefinitions::serialToN2k,
|
||||||
|
.readF=GwConfigDefinitions::serialReadF,
|
||||||
|
.writeF=GwConfigDefinitions::serialWriteF,
|
||||||
|
.name="Serial"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.id=SERIAL2_CHANNEL_ID,
|
||||||
|
.baud=GwConfigDefinitions::serial2Baud,
|
||||||
|
.receive=GwConfigDefinitions::receiveSerial2,
|
||||||
|
.send=GwConfigDefinitions::sendSerial2,
|
||||||
|
.direction=GwConfigDefinitions::serial2Dir,
|
||||||
|
.toN2K=GwConfigDefinitions::serial2ToN2k,
|
||||||
|
.readF=GwConfigDefinitions::serial2ReadF,
|
||||||
|
.writeF=GwConfigDefinitions::serial2WriteF,
|
||||||
|
.name="Serial2"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static SerialParam *getSerialParam(int id){
|
||||||
|
for (size_t idx=0;idx<sizeof(serialParameters)/sizeof(SerialParam*);idx++){
|
||||||
|
if (serialParameters[idx].id == id) return &serialParameters[idx];
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GwChannelList:: addSerial(HardwareSerial *stream,int id,int type,int rx,int tx){
|
||||||
|
const char *mode=nullptr;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case GWSERIAL_TYPE_UNI:
|
||||||
|
mode="UNI";
|
||||||
|
break;
|
||||||
|
case GWSERIAL_TYPE_BI:
|
||||||
|
mode="BI";
|
||||||
|
break;
|
||||||
|
case GWSERIAL_TYPE_RX:
|
||||||
|
mode="RX";
|
||||||
|
break;
|
||||||
|
case GWSERIAL_TYPE_TX:
|
||||||
|
mode="TX";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mode == nullptr) {
|
||||||
|
LOG_DEBUG(GwLog::ERROR,"unknown serial type %d",type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addSerial(stream,id,mode,rx,tx);
|
||||||
|
}
|
||||||
|
void GwChannelList::addSerial(HardwareSerial *serialStream,int id,const String &mode,int rx,int tx){
|
||||||
|
SerialParam *param=getSerialParam(id);
|
||||||
|
if (param == nullptr){
|
||||||
|
logger->logDebug(GwLog::ERROR,"trying to set up an unknown serial channel: %d",id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rx < 0 && tx < 0){
|
||||||
|
logger->logDebug(GwLog::ERROR,"useless config for serial %d: both rx/tx undefined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modes[id]=String(mode);
|
||||||
|
bool canRead=false;
|
||||||
|
bool canWrite=false;
|
||||||
|
if (mode == "BI"){
|
||||||
|
canRead=config->getBool(param->receive);
|
||||||
|
canWrite=config->getBool(param->send);
|
||||||
|
}
|
||||||
|
if (mode == "TX"){
|
||||||
|
canWrite=true;
|
||||||
|
}
|
||||||
|
if (mode == "RX"){
|
||||||
|
canRead=true;
|
||||||
|
}
|
||||||
|
if (mode == "UNI"){
|
||||||
|
String cfgMode=config->getString(param->direction);
|
||||||
|
if (cfgMode == "receive"){
|
||||||
|
canRead=true;
|
||||||
|
}
|
||||||
|
if (cfgMode == "send"){
|
||||||
|
canWrite=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rx < 0) canRead=false;
|
||||||
|
if (tx < 0) canWrite=false;
|
||||||
|
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d",
|
||||||
|
mode.c_str(),rx,(int)canRead,tx,(int)canWrite);
|
||||||
|
serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
|
||||||
|
GwSerial *serial = new GwSerial(logger, serialStream, id, canRead);
|
||||||
|
LOG_DEBUG(GwLog::LOG, "starting serial %d ", id);
|
||||||
|
GwChannel *channel = new GwChannel(logger, param->name, id);
|
||||||
|
channel->setImpl(serial);
|
||||||
|
channel->begin(
|
||||||
|
canRead || canWrite,
|
||||||
|
canWrite,
|
||||||
|
canRead,
|
||||||
|
config->getString(param->readF),
|
||||||
|
config->getString(param->writeF),
|
||||||
|
false,
|
||||||
|
config->getBool(param->toN2K),
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
LOG_DEBUG(GwLog::LOG, "%s", channel->toString().c_str());
|
||||||
|
theChannels.push_back(channel);
|
||||||
|
}
|
||||||
|
|
||||||
void GwChannelList::begin(bool fallbackSerial){
|
void GwChannelList::begin(bool fallbackSerial){
|
||||||
LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
|
LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
|
||||||
GwChannel *channel=NULL;
|
GwChannel *channel=NULL;
|
||||||
|
@ -85,7 +207,7 @@ void GwChannelList::begin(bool fallbackSerial){
|
||||||
//TCP server
|
//TCP server
|
||||||
sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
|
sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
|
||||||
sockets->begin();
|
sockets->begin();
|
||||||
channel=new GwChannel(logger,"TCP",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
|
channel=new GwChannel(logger,"TCPserver",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
|
||||||
channel->setImpl(sockets);
|
channel->setImpl(sockets);
|
||||||
channel->begin(
|
channel->begin(
|
||||||
true,
|
true,
|
||||||
|
@ -102,57 +224,33 @@ void GwChannelList::begin(bool fallbackSerial){
|
||||||
theChannels.push_back(channel);
|
theChannels.push_back(channel);
|
||||||
|
|
||||||
//serial 1
|
//serial 1
|
||||||
bool serCanRead=true;
|
#ifndef GWSERIAL_TX
|
||||||
bool serCanWrite=true;
|
#define GWSERIAL_TX -1
|
||||||
int serialrx=-1;
|
#endif
|
||||||
int serialtx=-1;
|
#ifndef GWSERIAL_RX
|
||||||
#ifdef GWSERIAL_MODE
|
#define GWSERIAL_RX -1
|
||||||
#ifdef GWSERIAL_TX
|
#endif
|
||||||
serialtx=GWSERIAL_TX;
|
#ifdef GWSERIAL_TYPE
|
||||||
#endif
|
addSerial(&Serial1,SERIAL1_CHANNEL_ID,GWSERIAL_TYPE,GWSERIAL_RX,GWSERIAL_TX);
|
||||||
#ifdef GWSERIAL_RX
|
#else
|
||||||
serialrx=GWSERIAL_RX;
|
#ifdef GWSERIAL_MODE
|
||||||
#endif
|
addSerial(&Serial1,SERIAL1_CHANNEL_ID,GWSERIAL_MODE,GWSERIAL_RX,GWSERIAL_TX);
|
||||||
if (serialrx != -1 && serialtx != -1){
|
#endif
|
||||||
serialMode=GWSERIAL_MODE;
|
#endif
|
||||||
}
|
//serial 2
|
||||||
|
#ifndef GWSERIAL2_TX
|
||||||
|
#define GWSERIAL2_TX -1
|
||||||
|
#endif
|
||||||
|
#ifndef GWSERIAL2_RX
|
||||||
|
#define GWSERIAL2_RX -1
|
||||||
|
#endif
|
||||||
|
#ifdef GWSERIAL2_TYPE
|
||||||
|
addSerial(&Serial2,SERIAL2_CHANNEL_ID,GWSERIAL2_TYPE,GWSERIAL2_RX,GWSERIAL2_TX);
|
||||||
|
#else
|
||||||
|
#ifdef GWSERIAL2_MODE
|
||||||
|
addSerial(&Serial2,SERIAL2_CHANNEL_ID,GWSERIAL2_MODE,GWSERIAL2_RX,GWSERIAL2_TX);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
//the serial direction is from the config (only valid for mode UNI)
|
|
||||||
String serialDirection=config->getString(config->serialDirection);
|
|
||||||
//we only consider the direction if mode is UNI
|
|
||||||
if (serialMode != String("UNI")){
|
|
||||||
serialDirection=String("");
|
|
||||||
//if mode is UNI it depends on the selection
|
|
||||||
serCanRead=config->getBool(config->receiveSerial);
|
|
||||||
serCanWrite=config->getBool(config->sendSerial);
|
|
||||||
}
|
|
||||||
if (serialDirection == "receive" || serialDirection == "off" || serialMode == "RX") serCanWrite=false;
|
|
||||||
if (serialDirection == "send" || serialDirection == "off" || serialMode == "TX") serCanRead=false;
|
|
||||||
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,direction=%s,rx=%d,tx=%d",
|
|
||||||
serialMode.c_str(),serialDirection.c_str(),serialrx,serialtx
|
|
||||||
);
|
|
||||||
if (serialtx != -1 || serialrx != -1 ){
|
|
||||||
LOG_DEBUG(GwLog::LOG,"creating serial interface rx=%d, tx=%d",serialrx,serialtx);
|
|
||||||
Serial1.begin(config->getInt(config->serialBaud,115200),SERIAL_8N1,serialrx,serialtx);
|
|
||||||
GwSerial *serial=new GwSerial(logger,&Serial1,SERIAL1_CHANNEL_ID,serCanRead);
|
|
||||||
LOG_DEBUG(GwLog::LOG,"starting serial1 ");
|
|
||||||
channel=new GwChannel(logger,"SER",SERIAL1_CHANNEL_ID);
|
|
||||||
channel->setImpl(serial);
|
|
||||||
channel->begin(
|
|
||||||
serCanRead || serCanWrite,
|
|
||||||
serCanWrite,
|
|
||||||
serCanRead,
|
|
||||||
config->getString(config->serialReadF),
|
|
||||||
config->getString(config->serialWriteF),
|
|
||||||
false,
|
|
||||||
config->getBool(config->serialToN2k),
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
|
|
||||||
theChannels.push_back(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
//tcp client
|
//tcp client
|
||||||
bool tclEnabled=config->getBool(config->tclEnabled);
|
bool tclEnabled=config->getBool(config->tclEnabled);
|
||||||
channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
|
channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
|
||||||
|
@ -180,6 +278,11 @@ void GwChannelList::begin(bool fallbackSerial){
|
||||||
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
|
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
|
||||||
logger->flush();
|
logger->flush();
|
||||||
}
|
}
|
||||||
|
String GwChannelList::getMode(int id){
|
||||||
|
auto it=modes.find(id);
|
||||||
|
if (it != modes.end()) return it->second;
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
int GwChannelList::getJsonSize(){
|
int GwChannelList::getJsonSize(){
|
||||||
int rt=0;
|
int rt=0;
|
||||||
allChannels([&](GwChannel *c){
|
allChannels([&](GwChannel *c){
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
#include <WString.h>
|
#include <WString.h>
|
||||||
#include "GwChannel.h"
|
#include "GwChannel.h"
|
||||||
#include "GwLog.h"
|
#include "GwLog.h"
|
||||||
#include "GWConfig.h"
|
#include "GWConfig.h"
|
||||||
#include "GwJsonDocument.h"
|
#include "GwJsonDocument.h"
|
||||||
#include "GwApi.h"
|
#include "GwApi.h"
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
//NMEA message channels
|
//NMEA message channels
|
||||||
#define N2K_CHANNEL_ID 0
|
#define N2K_CHANNEL_ID 0
|
||||||
|
@ -25,10 +27,11 @@ class GwChannelList{
|
||||||
GwConfigHandler *config;
|
GwConfigHandler *config;
|
||||||
typedef std::vector<GwChannel *> ChannelList;
|
typedef std::vector<GwChannel *> ChannelList;
|
||||||
ChannelList theChannels;
|
ChannelList theChannels;
|
||||||
|
std::map<int,String> modes;
|
||||||
GwSocketServer *sockets;
|
GwSocketServer *sockets;
|
||||||
GwTcpClient *client;
|
GwTcpClient *client;
|
||||||
String serialMode=F("NONE");
|
void addSerial(HardwareSerial *stream,int id,const String &mode,int rx,int tx);
|
||||||
|
void addSerial(HardwareSerial *stream,int id,int type,int rx,int tx);
|
||||||
public:
|
public:
|
||||||
GwChannelList(GwLog *logger, GwConfigHandler *config);
|
GwChannelList(GwLog *logger, GwConfigHandler *config);
|
||||||
typedef std::function<void(GwChannel *)> ChannelAction;
|
typedef std::function<void(GwChannel *)> ChannelAction;
|
||||||
|
@ -41,6 +44,6 @@ class GwChannelList{
|
||||||
//single channel
|
//single channel
|
||||||
GwChannel *getChannelById(int sourceId);
|
GwChannel *getChannelById(int sourceId);
|
||||||
void fillStatus(GwApi::Status &status);
|
void fillStatus(GwApi::Status &status);
|
||||||
|
String getMode(int id);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
#define GWSERIAL_TX 26
|
#define GWSERIAL_TX 26
|
||||||
#define GWSERIAL_RX 32
|
#define GWSERIAL_RX 32
|
||||||
#define GWSERIAL_MODE "UNI"
|
#define GWSERIAL_MODE "UNI"
|
||||||
|
|
||||||
|
#define GWSERIAL2_TX 14
|
||||||
|
#define GWSERIAL2_RX 15
|
||||||
|
#define GWSERIAL2_MODE "BI"
|
||||||
|
|
||||||
#define GWBUTTON_PIN GPIO_NUM_39
|
#define GWBUTTON_PIN GPIO_NUM_39
|
||||||
#define GWBUTTON_ACTIVE LOW
|
#define GWBUTTON_ACTIVE LOW
|
||||||
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
|
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
|
||||||
|
|
|
@ -13,6 +13,11 @@
|
||||||
*/
|
*/
|
||||||
#ifndef _GWHARDWARE_H
|
#ifndef _GWHARDWARE_H
|
||||||
#define _GWHARDWARE_H
|
#define _GWHARDWARE_H
|
||||||
|
#define GWSERIAL_TYPE_UNI 1
|
||||||
|
#define GWSERIAL_TYPE_BI 2
|
||||||
|
#define GWSERIAL_TYPE_RX 3
|
||||||
|
#define GWSERIAL_TYPE_TX 4
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include "GwUserTasks.h"
|
#include "GwUserTasks.h"
|
||||||
|
|
||||||
|
@ -117,7 +122,7 @@
|
||||||
#define ESP32_CAN_RX_PIN GPIO_NUM_4
|
#define ESP32_CAN_RX_PIN GPIO_NUM_4
|
||||||
//serial input only
|
//serial input only
|
||||||
#define GWSERIAL_RX GPIO_NUM_16
|
#define GWSERIAL_RX GPIO_NUM_16
|
||||||
#define GWSERIAL_MODE "RX"
|
#define GWSERIAL_TYPE GWSERIAL_TYPE_RX
|
||||||
|
|
||||||
#define GWBUTTON_PIN GPIO_NUM_0
|
#define GWBUTTON_PIN GPIO_NUM_0
|
||||||
#define GWBUTTON_ACTIVE LOW
|
#define GWBUTTON_ACTIVE LOW
|
||||||
|
@ -132,26 +137,26 @@
|
||||||
#ifdef SERIAL_GROOVE_485
|
#ifdef SERIAL_GROOVE_485
|
||||||
#define GWSERIAL_TX GROOVE_PIN_1
|
#define GWSERIAL_TX GROOVE_PIN_1
|
||||||
#define GWSERIAL_RX GROOVE_PIN_2
|
#define GWSERIAL_RX GROOVE_PIN_2
|
||||||
#define GWSERIAL_MODE "UNI"
|
#define GWSERIAL_TYPE GWSERIAL_TYPE_UNI
|
||||||
#endif
|
#endif
|
||||||
#ifdef SERIAL_GROOVE_232
|
#ifdef SERIAL_GROOVE_232
|
||||||
#define GWSERIAL_TX GROOVE_PIN_1
|
#define GWSERIAL_TX GROOVE_PIN_1
|
||||||
#define GWSERIAL_RX GROOVE_PIN_2
|
#define GWSERIAL_RX GROOVE_PIN_2
|
||||||
#define GWSERIAL_MODE "BI"
|
#define GWSERIAL_TYPE GWSERIAL_TYPE_BI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//M5 Serial (Atomic RS232 Base)
|
//M5 Serial (Atomic RS232 Base)
|
||||||
#ifdef M5_SERIAL_KIT_232
|
#ifdef M5_SERIAL_KIT_232
|
||||||
#define GWSERIAL_TX BOARD_LEFT2
|
#define GWSERIAL_TX BOARD_LEFT2
|
||||||
#define GWSERIAL_RX BOARD_LEFT1
|
#define GWSERIAL_RX BOARD_LEFT1
|
||||||
#define GWSERIAL_MODE "BI"
|
#define GWSERIAL_TYPE GWSERIAL_TYPE_BI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//M5 Serial (Atomic RS485 Base)
|
//M5 Serial (Atomic RS485 Base)
|
||||||
#ifdef M5_SERIAL_KIT_485
|
#ifdef M5_SERIAL_KIT_485
|
||||||
#define GWSERIAL_TX BOARD_LEFT2
|
#define GWSERIAL_TX BOARD_LEFT2
|
||||||
#define GWSERIAL_RX BOARD_LEFT1
|
#define GWSERIAL_RX BOARD_LEFT1
|
||||||
#define GWSERIAL_MODE "UNI"
|
#define GWSERIAL_TYPE GWSERIAL_TYPE_UNI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//can kit for M5 Atom
|
//can kit for M5 Atom
|
||||||
|
@ -165,5 +170,4 @@
|
||||||
#define ESP32_CAN_RX_PIN GROOVE_PIN_2
|
#define ESP32_CAN_RX_PIN GROOVE_PIN_2
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -9,12 +9,18 @@ static const int TIMEOUT_OFFLINE=256; //# of timeouts to consider offline
|
||||||
Nmea2kTwai::Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP, unsigned long logP):
|
Nmea2kTwai::Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP, unsigned long logP):
|
||||||
tNMEA2000(),RxPin(_RxPin),TxPin(_TxPin)
|
tNMEA2000(),RxPin(_RxPin),TxPin(_TxPin)
|
||||||
{
|
{
|
||||||
timers.addAction(logP,[this](){logStatus();});
|
if (RxPin < 0 || TxPin < 0){
|
||||||
timers.addAction(recP,[this](){checkRecovery();});
|
disabled=true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
timers.addAction(logP,[this](){logStatus();});
|
||||||
|
timers.addAction(recP,[this](){checkRecovery();});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent)
|
bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent)
|
||||||
{
|
{
|
||||||
|
if (disabled) return true;
|
||||||
twai_message_t message;
|
twai_message_t message;
|
||||||
memset(&message,0,sizeof(message));
|
memset(&message,0,sizeof(message));
|
||||||
message.identifier = id;
|
message.identifier = id;
|
||||||
|
@ -35,6 +41,10 @@ bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigne
|
||||||
}
|
}
|
||||||
bool Nmea2kTwai::CANOpen()
|
bool Nmea2kTwai::CANOpen()
|
||||||
{
|
{
|
||||||
|
if (disabled){
|
||||||
|
logDebug(LOG_INFO,"CAN disabled");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
esp_err_t rt=twai_start();
|
esp_err_t rt=twai_start();
|
||||||
if (rt != ESP_OK){
|
if (rt != ESP_OK){
|
||||||
logDebug(LOG_ERR,"CANOpen failed: %x",(int)rt);
|
logDebug(LOG_ERR,"CANOpen failed: %x",(int)rt);
|
||||||
|
@ -47,6 +57,7 @@ bool Nmea2kTwai::CANOpen()
|
||||||
}
|
}
|
||||||
bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf)
|
bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf)
|
||||||
{
|
{
|
||||||
|
if (disabled) return false;
|
||||||
twai_message_t message;
|
twai_message_t message;
|
||||||
esp_err_t rt=twai_receive(&message,0);
|
esp_err_t rt=twai_receive(&message,0);
|
||||||
if (rt != ESP_OK){
|
if (rt != ESP_OK){
|
||||||
|
@ -68,6 +79,7 @@ bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned cha
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void Nmea2kTwai::initDriver(){
|
void Nmea2kTwai::initDriver(){
|
||||||
|
if (disabled) return;
|
||||||
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TxPin,RxPin, TWAI_MODE_NORMAL);
|
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TxPin,RxPin, TWAI_MODE_NORMAL);
|
||||||
g_config.tx_queue_len=20;
|
g_config.tx_queue_len=20;
|
||||||
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS();
|
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS();
|
||||||
|
@ -84,13 +96,22 @@ void Nmea2kTwai::initDriver(){
|
||||||
// and you want to change size of library send frame buffer size. See e.g. NMEA2000_teensy.cpp.
|
// and you want to change size of library send frame buffer size. See e.g. NMEA2000_teensy.cpp.
|
||||||
void Nmea2kTwai::InitCANFrameBuffers()
|
void Nmea2kTwai::InitCANFrameBuffers()
|
||||||
{
|
{
|
||||||
initDriver();
|
if (disabled){
|
||||||
|
logDebug(LOG_INFO,"twai init - disabled");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
initDriver();
|
||||||
|
}
|
||||||
tNMEA2000::InitCANFrameBuffers();
|
tNMEA2000::InitCANFrameBuffers();
|
||||||
|
|
||||||
}
|
}
|
||||||
Nmea2kTwai::Status Nmea2kTwai::getStatus(){
|
Nmea2kTwai::Status Nmea2kTwai::getStatus(){
|
||||||
twai_status_info_t state;
|
twai_status_info_t state;
|
||||||
Status rt;
|
Status rt;
|
||||||
|
if (disabled){
|
||||||
|
rt.state=ST_DISABLED;
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
if (twai_get_status_info(&state) != ESP_OK){
|
if (twai_get_status_info(&state) != ESP_OK){
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
@ -120,6 +141,7 @@ Nmea2kTwai::Status Nmea2kTwai::getStatus(){
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
bool Nmea2kTwai::checkRecovery(){
|
bool Nmea2kTwai::checkRecovery(){
|
||||||
|
if (disabled) return false;
|
||||||
Status canState=getStatus();
|
Status canState=getStatus();
|
||||||
bool strt=false;
|
bool strt=false;
|
||||||
if (canState.state != Nmea2kTwai::ST_RUNNING)
|
if (canState.state != Nmea2kTwai::ST_RUNNING)
|
||||||
|
@ -140,6 +162,7 @@ bool Nmea2kTwai::checkRecovery(){
|
||||||
}
|
}
|
||||||
|
|
||||||
void Nmea2kTwai::loop(){
|
void Nmea2kTwai::loop(){
|
||||||
|
if (disabled) return;
|
||||||
timers.loop();
|
timers.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +180,7 @@ Nmea2kTwai::Status Nmea2kTwai::logStatus(){
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Nmea2kTwai::startRecovery(){
|
bool Nmea2kTwai::startRecovery(){
|
||||||
|
if (disabled) return false;
|
||||||
lastRecoveryStart=millis();
|
lastRecoveryStart=millis();
|
||||||
esp_err_t rt=twai_driver_uninstall();
|
esp_err_t rt=twai_driver_uninstall();
|
||||||
if (rt != ESP_OK){
|
if (rt != ESP_OK){
|
||||||
|
@ -174,6 +198,7 @@ const char * Nmea2kTwai::stateStr(const Nmea2kTwai::STATE &st){
|
||||||
case ST_RUNNING: return "RUNNING";
|
case ST_RUNNING: return "RUNNING";
|
||||||
case ST_STOPPED: return "STOPPED";
|
case ST_STOPPED: return "STOPPED";
|
||||||
case ST_OFFLINE: return "OFFLINE";
|
case ST_OFFLINE: return "OFFLINE";
|
||||||
|
case ST_DISABLED: return "DISABLED";
|
||||||
}
|
}
|
||||||
return "ERROR";
|
return "ERROR";
|
||||||
}
|
}
|
|
@ -12,6 +12,7 @@ class Nmea2kTwai : public tNMEA2000{
|
||||||
ST_BUS_OFF,
|
ST_BUS_OFF,
|
||||||
ST_RECOVERING,
|
ST_RECOVERING,
|
||||||
ST_OFFLINE,
|
ST_OFFLINE,
|
||||||
|
ST_DISABLED,
|
||||||
ST_ERROR
|
ST_ERROR
|
||||||
} STATE;
|
} STATE;
|
||||||
typedef struct{
|
typedef struct{
|
||||||
|
@ -55,6 +56,7 @@ class Nmea2kTwai : public tNMEA2000{
|
||||||
gpio_num_t RxPin;
|
gpio_num_t RxPin;
|
||||||
uint32_t txTimeouts=0;
|
uint32_t txTimeouts=0;
|
||||||
GwIntervalRunner timers;
|
GwIntervalRunner timers;
|
||||||
|
bool disabled=false;
|
||||||
unsigned long lastRecoveryStart=0;
|
unsigned long lastRecoveryStart=0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
17
src/main.cpp
17
src/main.cpp
|
@ -126,7 +126,7 @@ class Nmea2kTwaiLog : public Nmea2kTwai{
|
||||||
#define ESP32_CAN_RX_PIN GPIO_NUM_NC
|
#define ESP32_CAN_RX_PIN GPIO_NUM_NC
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog(ESP32_CAN_TX_PIN,ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger));
|
Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger));
|
||||||
|
|
||||||
#ifdef GWBUTTON_PIN
|
#ifdef GWBUTTON_PIN
|
||||||
bool fixedApPass=false;
|
bool fixedApPass=false;
|
||||||
|
@ -155,8 +155,8 @@ SemaphoreHandle_t mainLock;
|
||||||
GwRequestQueue mainQueue(&logger,20);
|
GwRequestQueue mainQueue(&logger,20);
|
||||||
GwWebServer webserver(&logger,&mainQueue,80);
|
GwWebServer webserver(&logger,&mainQueue,80);
|
||||||
|
|
||||||
GwCounter<unsigned long> countNMEA2KIn("count2Kin");
|
GwCounter<unsigned long> countNMEA2KIn("countNMEA2000in");
|
||||||
GwCounter<unsigned long> countNMEA2KOut("count2Kout");
|
GwCounter<unsigned long> countNMEA2KOut("countNMEA2000out");
|
||||||
GwIntervalRunner timers;
|
GwIntervalRunner timers;
|
||||||
|
|
||||||
bool checkPass(String hash){
|
bool checkPass(String hash){
|
||||||
|
@ -399,6 +399,7 @@ protected:
|
||||||
}
|
}
|
||||||
status["n2kstate"]=NMEA2000.stateStr(driverState);
|
status["n2kstate"]=NMEA2000.stateStr(driverState);
|
||||||
status["n2knode"]=NodeAddress;
|
status["n2knode"]=NodeAddress;
|
||||||
|
status["minUser"]=MIN_USER_TASK;
|
||||||
//nmea0183Converter->toJson(status);
|
//nmea0183Converter->toJson(status);
|
||||||
countNMEA2KIn.toJson(status);
|
countNMEA2KIn.toJson(status);
|
||||||
countNMEA2KOut.toJson(status);
|
countNMEA2KOut.toJson(status);
|
||||||
|
@ -427,17 +428,13 @@ class CapabilitiesRequest : public GwRequestMessage{
|
||||||
protected:
|
protected:
|
||||||
virtual void processRequest(){
|
virtual void processRequest(){
|
||||||
int numCapabilities=userCodeHandler.getCapabilities()->size();
|
int numCapabilities=userCodeHandler.getCapabilities()->size();
|
||||||
GwJsonDocument json(JSON_OBJECT_SIZE(numCapabilities*3+6));
|
GwJsonDocument json(JSON_OBJECT_SIZE(numCapabilities*3+8));
|
||||||
for (auto it=userCodeHandler.getCapabilities()->begin();
|
for (auto it=userCodeHandler.getCapabilities()->begin();
|
||||||
it != userCodeHandler.getCapabilities()->end();it++){
|
it != userCodeHandler.getCapabilities()->end();it++){
|
||||||
json[it->first]=it->second;
|
json[it->first]=it->second;
|
||||||
}
|
}
|
||||||
#ifdef GWSERIAL_MODE
|
json["serialmode"]=channels.getMode(SERIAL1_CHANNEL_ID);
|
||||||
String serial(F(GWSERIAL_MODE));
|
json["serial2mode"]=channels.getMode(SERIAL2_CHANNEL_ID);
|
||||||
#else
|
|
||||||
String serial(F("NONE"));
|
|
||||||
#endif
|
|
||||||
json["serialmode"]=serial;
|
|
||||||
#ifdef GWBUTTON_PIN
|
#ifdef GWBUTTON_PIN
|
||||||
json["hardwareReset"]="true";
|
json["hardwareReset"]="true";
|
||||||
#endif
|
#endif
|
||||||
|
|
122
web/config.json
122
web/config.json
|
@ -392,6 +392,128 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"category": "serial port"
|
"category": "serial port"
|
||||||
|
}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
"name": "serial2Dir",
|
||||||
|
"label": "serial2 direction",
|
||||||
|
"type": "list",
|
||||||
|
"default": "receive",
|
||||||
|
"list": [
|
||||||
|
"send",
|
||||||
|
"receive",
|
||||||
|
"off"
|
||||||
|
],
|
||||||
|
"description": "use the serial2 port to send or receive data",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"UNI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serial2Baud",
|
||||||
|
"label": "serial2 baud rate",
|
||||||
|
"type": "list",
|
||||||
|
"default": "115200",
|
||||||
|
"description": "baud rate for the serial port 2",
|
||||||
|
"list": [
|
||||||
|
1200,
|
||||||
|
2400,
|
||||||
|
4800,
|
||||||
|
9600,
|
||||||
|
14400,
|
||||||
|
19200,
|
||||||
|
28800,
|
||||||
|
38400,
|
||||||
|
57600,
|
||||||
|
115200,
|
||||||
|
230400,
|
||||||
|
460800
|
||||||
|
],
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"RX",
|
||||||
|
"TX",
|
||||||
|
"UNI",
|
||||||
|
"BI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sendSerial2",
|
||||||
|
"label": "NMEA to Serial2",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "true",
|
||||||
|
"description": "send out NMEA data on the serial port 2",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"TX",
|
||||||
|
"BI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "receiveSerial2",
|
||||||
|
"label": "NMEA from Serial2",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "true",
|
||||||
|
"description": "receive NMEA data on the serial port 2",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"RX",
|
||||||
|
"BI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serial2ToN2k",
|
||||||
|
"label": "serial2 to NMEA2000",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "true",
|
||||||
|
"description": "convert NMEA0183 from the serial port 2 to NMEA2000",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"RX",
|
||||||
|
"BI",
|
||||||
|
"UNI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serial2ReadF",
|
||||||
|
"label": "serial2 read Filter",
|
||||||
|
"type": "filter",
|
||||||
|
"default": "",
|
||||||
|
"description": "filter for NMEA0183 data when reading from serial2\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"RX",
|
||||||
|
"BI",
|
||||||
|
"UNI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serial2WriteF",
|
||||||
|
"label": "serial2 write Filter",
|
||||||
|
"type": "filter",
|
||||||
|
"default": "",
|
||||||
|
"description": "filter for NMEA0183 data when writing to serial2\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
|
||||||
|
"capabilities": {
|
||||||
|
"serial2mode": [
|
||||||
|
"TX",
|
||||||
|
"BI",
|
||||||
|
"UNI"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"category": "serial2 port"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "serverPort",
|
"name": "serverPort",
|
||||||
|
|
72
web/index.js
72
web/index.js
|
@ -3,6 +3,8 @@ let lastUpdate = (new Date()).getTime();
|
||||||
let reloadConfig = false;
|
let reloadConfig = false;
|
||||||
let needAdminPass=true;
|
let needAdminPass=true;
|
||||||
let lastSalt="";
|
let lastSalt="";
|
||||||
|
let channelList={};
|
||||||
|
let minUser=200;
|
||||||
function addEl(type, clazz, parent, text) {
|
function addEl(type, clazz, parent, text) {
|
||||||
let el = document.createElement(type);
|
let el = document.createElement(type);
|
||||||
if (clazz) {
|
if (clazz) {
|
||||||
|
@ -65,22 +67,39 @@ function update() {
|
||||||
}
|
}
|
||||||
getJson('/api/status')
|
getJson('/api/status')
|
||||||
.then(function (jsonData) {
|
.then(function (jsonData) {
|
||||||
|
let statusPage=document.getElementById('statusPageContent');
|
||||||
|
let even=true; //first counter
|
||||||
for (let k in jsonData) {
|
for (let k in jsonData) {
|
||||||
if (k == "salt"){
|
if (k == "salt"){
|
||||||
lastSalt=jsonData[k];
|
lastSalt=jsonData[k];
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (k == "minUser"){
|
||||||
|
minUser=parseInt(jsonData[k]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! statusPage) continue;
|
||||||
if (typeof (jsonData[k]) === 'object') {
|
if (typeof (jsonData[k]) === 'object') {
|
||||||
for (let sk in jsonData[k]) {
|
if (k.indexOf('count') == 0) {
|
||||||
let key = k + "." + sk;
|
createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/," in").replace(/out$/," out"), k, even);
|
||||||
if (typeof (jsonData[k][sk]) === 'object') {
|
even = !even;
|
||||||
//msg details
|
for (let sk in jsonData[k]) {
|
||||||
updateMsgDetails(key, jsonData[k][sk]);
|
let key = k + "." + sk;
|
||||||
}
|
if (typeof (jsonData[k][sk]) === 'object') {
|
||||||
else {
|
//msg details
|
||||||
let el = document.getElementById(key);
|
updateMsgDetails(key, jsonData[k][sk]);
|
||||||
if (el) el.textContent = jsonData[k][sk];
|
}
|
||||||
|
else {
|
||||||
|
let el = document.getElementById(key);
|
||||||
|
if (el) el.textContent = jsonData[k][sk];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (k.indexOf("ch")==0){
|
||||||
|
//channel def
|
||||||
|
let name=k.substring(2);
|
||||||
|
channelList[name]=jsonData[k];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let el = document.getElementById(k);
|
let el = document.getElementById(k);
|
||||||
|
@ -286,9 +305,13 @@ function factoryReset() {
|
||||||
.catch(function (e) { });
|
.catch(function (e) { });
|
||||||
}
|
}
|
||||||
function createCounterDisplay(parent,label,key,isEven){
|
function createCounterDisplay(parent,label,key,isEven){
|
||||||
|
if (parent.querySelector("#"+key)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
let clazz="row icon-row counter-row";
|
let clazz="row icon-row counter-row";
|
||||||
if (isEven) clazz+=" even";
|
if (isEven) clazz+=" even";
|
||||||
let row=addEl('div',clazz,parent);
|
let row=addEl('div',clazz,parent);
|
||||||
|
row.setAttribute("id",key);
|
||||||
let icon=addEl('span','icon icon-more',row);
|
let icon=addEl('span','icon icon-more',row);
|
||||||
addEl('span','label',row,label);
|
addEl('span','label',row,label);
|
||||||
let value=addEl('span','value',row,'---');
|
let value=addEl('span','value',row,'---');
|
||||||
|
@ -331,18 +354,7 @@ function updateMsgDetails(key, details) {
|
||||||
},frame);
|
},frame);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let counters={
|
|
||||||
count2Kin: 'NMEA2000 in',
|
|
||||||
count2Kout: 'NMEA2000 out',
|
|
||||||
countTCPin: 'TCPserver in',
|
|
||||||
countTCPout: 'TCPserver out',
|
|
||||||
countTCPClientin: 'TCPclient in',
|
|
||||||
countTCPClientout: 'TCPclient out',
|
|
||||||
countUSBin: 'USB in',
|
|
||||||
countUSBout: 'USB out',
|
|
||||||
countSERin: 'Serial in',
|
|
||||||
countSERout: 'Serial out'
|
|
||||||
}
|
|
||||||
function showOverlay(text, isHtml) {
|
function showOverlay(text, isHtml) {
|
||||||
let el = document.getElementById('overlayContent');
|
let el = document.getElementById('overlayContent');
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
|
@ -1448,13 +1460,13 @@ function createDashboard() {
|
||||||
frame.innerHTML = '';
|
frame.innerHTML = '';
|
||||||
}
|
}
|
||||||
function sourceName(v){
|
function sourceName(v){
|
||||||
if (v == 0) return "N2K";
|
for (let n in channelList){
|
||||||
if (v == 1) return "USB";
|
if (v >= channelList[n].id && v <= channelList[n].max){
|
||||||
if (v == 2) return "SER";
|
return n;
|
||||||
if (v == 3) return "TCPcl"
|
}
|
||||||
if (v >= 4 && v <= 20) return "TCPser";
|
}
|
||||||
if (v >= 200) return "USER";
|
if (v < minUser) return "---";
|
||||||
return "---";
|
return "USER["+v+"]";
|
||||||
}
|
}
|
||||||
let lastSelectList=[];
|
let lastSelectList=[];
|
||||||
function updateDashboard(data) {
|
function updateDashboard(data) {
|
||||||
|
@ -1716,13 +1728,13 @@ window.addEventListener('load', function () {
|
||||||
}
|
}
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
let statusPage=document.getElementById('statusPageContent');
|
let statusPage=document.getElementById('statusPageContent');
|
||||||
if (statusPage){
|
/*if (statusPage){
|
||||||
let even=true;
|
let even=true;
|
||||||
for (let c in counters){
|
for (let c in counters){
|
||||||
createCounterDisplay(statusPage,counters[c],c,even);
|
createCounterDisplay(statusPage,counters[c],c,even);
|
||||||
even=!even;
|
even=!even;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
forEl('#uploadFile',function(el){
|
forEl('#uploadFile',function(el){
|
||||||
el.addEventListener('change',function(ev){
|
el.addEventListener('change',function(ev){
|
||||||
if (ev.target.files.length < 1) return;
|
if (ev.target.files.length < 1) return;
|
||||||
|
|
Loading…
Reference in New Issue