bufDel(buf);
msg.GetMessage(buf,MAX_NMEA0183_MSG_BUF_LEN);
result=buf;
}
@@ -707,75 +582,19 @@ void setup() {
uint8_t chipid[6];
uint32_t id = 0;
config.loadConfig();
- // Init USB serial port
- GwConfigInterface *usbBaud=config.getConfigItem(config.usbBaud,false);
- int baud=115200;
- if (usbBaud){
- baud=usbBaud->asInt();
- }
+ bool fallbackSerial=false;
#ifdef FALLBACK_SERIAL
- int st=-1;
-#else
- int st=usbSerial->setup(baud,3,1); //TODO: PIN defines
-#endif
- if (st < 0){
+ fallbackSerial=true;
//falling back to old style serial for logging
Serial.begin(baud);
Serial.printf("fallback serial enabled, error was %d\n",st);
logger.prefix="FALLBACK:";
- }
- else{
- logger.prefix="GWSERIAL:";
- logger.setWriter(&logWriter);
- logger.logDebug(GwLog::LOG,"created GwSerial for USB port");
- }
- logger.logDebug(GwLog::LOG,"config: %s", config.toString().c_str());
+#endif
userCodeHandler.startInitTasks(MIN_USER_TASK);
- #ifdef GWSERIAL_MODE
- int serialrx=-1;
- int serialtx=-1;
- #ifdef GWSERIAL_TX
- serialtx=GWSERIAL_TX;
- #endif
- #ifdef GWSERIAL_RX
- serialrx=GWSERIAL_RX;
- #endif
- //the mode is a compile time preselection from hardware.h
- String serialMode(F(GWSERIAL_MODE));
- //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=receiveSerial->asBoolean();
- serCanWrite=sendSerial->asBoolean();
- }
- if (serialDirection == "receive" || serialDirection == "off" || serialMode == "RX") serCanWrite=false;
- if (serialDirection == "send" || serialDirection == "off" || serialMode == "TX") serCanRead=false;
- logger.logDebug(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){
- logger.logDebug(GwLog::LOG,"creating serial interface rx=%d, tx=%d",serialrx,serialtx);
- serial1=new GwSerial(&logger,1,SERIAL1_CHANNEL_ID,serCanRead);
- }
- if (serial1){
- int rt=serial1->setup(config.getInt(config.serialBaud,115200),serialrx,serialtx);
- logger.logDebug(GwLog::LOG,"starting serial returns %d",rt);
- }
- #endif
-
- MDNS.begin(config.getConfigItem(config.systemName)->asCString());
gwWifi.setup();
-
- // Start TCP server
- socketServer.begin();
+ MDNS.begin(config.getConfigItem(config.systemName)->asCString());
+ channels.begin(fallbackSerial);
logger.flush();
-
- logger.logDebug(GwLog::LOG,"usbRead: %s", usbReadFilter.toString().c_str());
- logger.flush();
-
webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{
return new ResetRequest(request->arg("_hash"));
});
@@ -829,15 +648,15 @@ void setup() {
[](const tNMEA0183Msg &msg, int sourceId){
SendNMEA0183Message(msg,sourceId,false);
}
- , N2K_CHANNEL_ID,
+ ,
config.getString(config.talkerId,String("GP")),
&xdrMappings,
config.getInt(config.minXdrInterval,100)
);
- toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg)->bool{
+ toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg, int sourceId)->bool{
logger.logDebug(GwLog::DEBUG+2,"send N2K %ld",msg.PGN);
- handleN2kMessage(msg,N2KT_MSGOUT);
+ handleN2kMessage(msg,sourceId,true);
return true;
},
&xdrMappings,
@@ -892,17 +711,8 @@ void setup() {
}
NMEA2000.ExtendTransmitMessages(pgns);
NMEA2000.ExtendReceiveMessages(nmea0183Converter->handledPgns());
- if (usbActisense->asBoolean()){
- actisenseReader=new tActisenseReader();
- usbStream=usbSerial->getStream(false);
- actisenseReader->SetReadStream(usbStream);
- actisenseReader->SetMsgHandler([](const tN2kMsg &msg){
- countUSBIn.add(String(msg.PGN));
- handleN2kMessage(msg,N2KT_MSGACT);
- });
- }
NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){
- handleN2kMessage(n2kMsg,N2KT_MSGIN);
+ handleN2kMessage(n2kMsg,N2K_CHANNEL_ID);
});
NMEA2000.Open();
logger.logDebug(GwLog::LOG,"starting addon tasks");
@@ -920,49 +730,12 @@ void setup() {
}
//*****************************************************************************
void handleSendAndRead(bool handleRead){
- socketServer.loop(handleRead);
- usbSerial->loop(handleRead);
- if (serial1) serial1->loop(handleRead);
+ channels.allChannels([&](GwChannel *c){
+ c->loop(handleRead,true);
+ });
}
-class NMEAMessageReceiver : public GwMessageFetcher{
- static const int bufferSize=GwBuffer::RX_BUFFER_SIZE+4;
- uint8_t buffer[bufferSize];
- uint8_t *writePointer=buffer;
- public:
- virtual bool handleBuffer(GwBuffer *gwbuffer){
- size_t len=fetchMessageToBuffer(gwbuffer,buffer,bufferSize-4,'\n');
- writePointer=buffer+len;
- if (writePointer == buffer) return false;
- uint8_t *p;
- for (p=writePointer-1;p>=buffer && *p <= 0x20;p--){
- *p=0;
- }
- if (p > buffer){
- p++;
- *p=0x0d;
- p++;
- *p=0x0a;
- p++;
- *p=0;
- }
- for (p=buffer; *p != 0 && p < writePointer && *p <= 0x20;p++){}
- //very simple NMEA check
- if (*p != '!' && *p != '$'){
- logger.logDebug(GwLog::DEBUG,"unknown line [%d] - ignore: %s",id,(const char *)p);
- }
- else{
- logger.logDebug(GwLog::DEBUG,"NMEA[%d]: %s",id,(const char *)p);
- handleReceivedNmeaMessage((const char *)p,id);
- //trigger sending to empty buffers
- handleSendAndRead(false);
- }
- writePointer=buffer;
- return true;
- }
-};
TimeMonitor monitor(20,0.2);
-NMEAMessageReceiver receiver;
unsigned long lastHeapReport=0;
void loop() {
monitor.reset();
@@ -983,20 +756,18 @@ void loop() {
}
}
monitor.setTime(3);
- //read sockets
- socketServer.loop(true,false);
+ channels.allChannels([](GwChannel *c){
+ c->loop(true,false);
+ });
+ //reads
monitor.setTime(4);
- //write sockets
- socketServer.loop(false,true);
+ channels.allChannels([](GwChannel *c){
+ c->loop(false,true);
+ });
+ //writes
monitor.setTime(5);
- usbSerial->loop(true);
- monitor.setTime(6);
- if (serial1) serial1->loop(true);
- monitor.setTime(7);
- handleSendAndRead(true);
- monitor.setTime(8);
NMEA2000.ParseMessages();
- monitor.setTime(9);
+ monitor.setTime(6);
int SourceAddress = NMEA2000.GetN2kSource();
if (SourceAddress != NodeAddress) { // Save potentially changed Source Address to NVS memory
@@ -1007,21 +778,36 @@ void loop() {
logger.logDebug(GwLog::LOG,"Address Change: New Address=%d\n", SourceAddress);
}
nmea0183Converter->loop();
- monitor.setTime(10);
+ monitor.setTime(7);
//read channels
- if (readTCP->asBoolean()) socketServer.readMessages(&receiver);
- monitor.setTime(11);
- receiver.id=USB_CHANNEL_ID;
- if (! actisenseReader && readUsb->asBoolean()) usbSerial->readMessages(&receiver);
- monitor.setTime(12);
- receiver.id=SERIAL1_CHANNEL_ID;
- if (serial1 && serCanRead ) serial1->readMessages(&receiver);
- monitor.setTime(13);
- if (actisenseReader){
- actisenseReader->ParseMessages();
- }
- monitor.setTime(14);
+ channels.allChannels([](GwChannel *c){
+ c->readMessages([&](const char * buffer, int sourceId){
+ channels.allChannels([&](GwChannel *oc){
+ oc->sendToClients(buffer,sourceId);
+ oc->loop(false,true);
+ });
+ if (c->sendToN2K()){
+ if (strlen(buffer) > 6 && strncmp(buffer,"$PCDIN",6) == 0){
+ tN2kMsg n2kMsg;
+ uint32_t timestamp;
+ if (SeasmartToN2k(buffer,timestamp,n2kMsg)){
+ handleN2kMessage(n2kMsg,sourceId);
+ }
+ }
+ else{
+ toN2KConverter->parseAndSend(buffer, sourceId);
+ }
+ }
+ });
+ });
+ monitor.setTime(8);
+ channels.allChannels([](GwChannel *c){
+ c->parseActisense([](const tN2kMsg &msg,int source){
+ handleN2kMessage(msg,source);
+ });
+ });
+ monitor.setTime(9);
//handle message requests
GwMessage *msg=mainQueue.fetchMessage(0);
@@ -1029,5 +815,5 @@ void loop() {
msg->process();
msg->unref();
}
- monitor.setTime(15);
+ monitor.setTime(10);
}
diff --git a/tools/buildFlashtool.sh b/tools/buildFlashtool.sh
new file mode 100755
index 0000000..a7a43e0
--- /dev/null
+++ b/tools/buildFlashtool.sh
@@ -0,0 +1,6 @@
+#! /bin/sh
+pdir=`dirname $0`
+cd $pdir || exit 1
+rm -rf flashtool/.idea
+python3 -m zipapp flashtool -m flashtool:main
+
diff --git a/tools/flashtool.py b/tools/flashtool.py
deleted file mode 100755
index 9e1d882..0000000
--- a/tools/flashtool.py
+++ /dev/null
@@ -1,237 +0,0 @@
-#! /usr/bin/env python3
-import tkinter as tk
-from tkinter import ttk
-import tkinter.font as tkFont
-
-import os
-import serial.tools.list_ports
-from tkinter import filedialog as FileDialog
-
-import builtins
-
-VERSION="1.0, esptool 3.2"
-
-oldprint=builtins.print
-def print(*args, **kwargs):
- app.addText(*args,**kwargs)
-
-
-builtins.print=print
-import esptool
-
-class App:
- def __init__(self, root):
- root.title("ESP32 NMEA2000 Flash Tool")
- root.geometry("800x600")
- root.resizable(width=True, height=True)
- root.configure(background='lightgrey')
- root.columnconfigure(0, weight=1)
- root.rowconfigure(0, weight=1)
- frame=tk.Frame(root)
- row=0
- frame.grid(row=0,column=0,sticky='news')
- #frame.configure(background='lightblue')
- frame.columnconfigure(0,weight=1)
- frame.columnconfigure(1, weight=3)
- tk.Label(frame,text="ESP32 NMEA2000 Flash Tool").grid(row=row,column=0,columnspan=2,sticky='ew')
- row+=1
- tk.Label(frame, text=VERSION).grid(row=row,column=0,columnspan=2,sticky="ew",pady=10)
- row+=1
- self.mode=tk.IntVar()
- self.mode.set(1)
- rdFrame=tk.Frame(frame)
- rdFrame.grid(row=row,column=1,sticky="ew",pady=20)
- tk.Radiobutton(rdFrame,text="Initial Flash",value=1,variable=self.mode,command=self.changeMode).grid(row=0,column=0)
- tk.Radiobutton(rdFrame, text="Update Flash", value=2, variable=self.mode,command=self.changeMode).grid(row=0,column=1)
- row+=1
- tk.Label(frame, text="Com Port").grid(row=row,column=0,sticky='ew')
- self.port=ttk.Combobox(frame)
- self.port.grid(row=row,column=1,sticky="ew")
- row+=1
- tk.Label(frame,text="Select Firmware").grid(row=row,column=0,sticky='ew')
- self.filename=tk.StringVar()
- fn=tk.Entry(frame,textvariable=self.filename)
- fn.grid(row=row,column=1,sticky='ew')
- fn.bind("<1>",self.fileDialog)
- row+=1
- self.fileInfo=tk.StringVar()
- tk.Label(frame,textvariable=self.fileInfo).grid(row=row,column=0,columnspan=2,sticky="ew")
- row+=1
- self.flashInfo=tk.StringVar()
- self.flashInfo.set("Address 0x1000")
- tk.Label(frame,textvariable=self.flashInfo).grid(row=row,column=0,columnspan=2,sticky='ew',pady=10)
- row+=1
- btFrame=tk.Frame(frame)
- btFrame.grid(row=row,column=0,columnspan=2,pady=15)
- self.actionButtons=[]
- bt=tk.Button(btFrame,text="Check",command=self.buttonCheck)
- bt.grid(row=0,column=0)
- self.actionButtons.append(bt)
- bt=tk.Button(btFrame, text="Flash", command=self.buttonFlash)
- bt.grid(row=0, column=1)
- self.actionButtons.append(bt)
- self.cancelButton=tk.Button(btFrame,text="Cancel",state=tk.DISABLED,command=self.buttonCancel)
- self.cancelButton.grid(row=0,column=2)
- row+=1
- self.text_widget = tk.Text(frame)
- frame.rowconfigure(row,weight=1)
- self.text_widget.grid(row=row,column=0,columnspan=2,sticky='news')
- self.readDevices()
- self.interrupt=False
-
- def updateFlashInfo(self):
- if self.mode.get() == 1:
- #full
- self.flashInfo.set("Address 0x1000")
- else:
- self.flashInfo.set("Erase(otadata): 0xe000...0xffff, Address 0x10000")
- def changeMode(self):
- m=self.mode.get()
- self.updateFlashInfo()
- self.filename.set('')
- self.fileInfo.set('')
- def fileDialog(self,ev):
- fn=FileDialog.askopenfilename()
- if fn:
- self.filename.set(fn)
- info=self.checkImageFile(fn,self.mode.get() == 1)
- if info['error']:
- self.fileInfo.set("***ERROR: %s"%info['info'])
- else:
- self.fileInfo.set(info['info'])
- def readDevices(self):
- self.serialDevices=[]
- names=[]
- for dev in serial.tools.list_ports.comports(False):
- self.serialDevices.append(dev.device)
- if dev.description != 'n/a':
- label=dev.description+"("+dev.device+")"
- else:
- label=dev.name+"("+dev.device+")"
- names.append(label)
- self.port.configure(values=names)
- def addText(self,*args,**kwargs):
- first=True
- for k in args:
- self.text_widget.insert(tk.END,k)
- if not first:
- self.text_widget.insert(tk.END, ',')
- first=False
- if kwargs.get('end') is None:
- self.text_widget.insert(tk.END,"\n")
- else:
- self.text_widget.insert(tk.END,kwargs.get('end'))
- self.text_widget.see('end')
- root.update()
- if self.interrupt:
- self.interrupt=False
- raise Exception("User cancel")
-
- FULLOFFSET=61440
- HDROFFSET = 288
- VERSIONOFFSET = 16
- NAMEOFFSET = 48
- MINSIZE = HDROFFSET + NAMEOFFSET + 32
- CHECKBYTES = {
- 0: 0xe9, # image magic
- 288: 0x32, # app header magic
- 289: 0x54,
- 290: 0xcd,
- 291: 0xab
- }
-
- def getString(self,buffer, offset, len):
- return buffer[offset:offset + len].rstrip(b'\0').decode('utf-8')
-
- def getFirmwareInfo(self,ih,imageFile,offset):
- buffer = ih.read(self.MINSIZE)
- if len(buffer) != self.MINSIZE:
- return self.setErr("invalid image file %s, to short"%imageFile)
- for k, v in self.CHECKBYTES.items():
- if buffer[k] != v:
- return self.setErr("invalid magic at %d, expected %d got %d"
- % (k+offset, v, buffer[k]))
- name = self.getString(buffer, self.HDROFFSET + self.NAMEOFFSET, 32)
- version = self.getString(buffer, self.HDROFFSET + self.VERSIONOFFSET, 32)
- return {'error':False,'info':"%s:%s"%(name,version)}
-
- def setErr(self,err):
- return {'error':True,'info':err}
- def checkImageFile(self,filename,isFull):
- if not os.path.exists(filename):
- return self.setErr("file %s not found"%filename)
- with open(filename,"rb") as fh:
- offset=0
- if isFull:
- b=fh.read(1)
- if len(b) != 1:
- return self.setErr("unable to read header")
- if b[0] != 0xe9:
- return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%b[0])
- st=fh.seek(self.FULLOFFSET)
- offset=self.FULLOFFSET
- return self.getFirmwareInfo(fh,filename,offset)
-
- def runCheck(self):
- self.text_widget.delete("1.0", "end")
- idx = self.port.current()
- isFull = self.mode.get() == 1
- if idx < 0:
- self.addText("ERROR: no com port selected")
- return
- port = self.serialDevices[idx]
- fn = self.filename.get()
- if fn is None or fn == '':
- self.addText("ERROR: no filename selected")
- return
- info = self.checkImageFile(fn, isFull)
- if info['error']:
- print("ERROR: %s" % info['info'])
- return
- return {'port':port,'isFull':isFull}
-
- def runEspTool(self,command):
- for b in self.actionButtons:
- b.configure(state=tk.DISABLED)
- self.cancelButton.configure(state=tk.NORMAL)
- print("run esptool: %s" % " ".join(command))
- root.update()
- root.update_idletasks()
- try:
- esptool.main(command)
- print("esptool done")
- except Exception as e:
- print("Exception in esptool %s" % e)
- for b in self.actionButtons:
- b.configure(state=tk.NORMAL)
- self.cancelButton.configure(state=tk.DISABLED)
- def buttonCheck(self):
- param = self.runCheck()
- if not param:
- return
- print("Settings OK")
- command = ['--chip', 'ESP32', '--port', param['port'], 'chip_id']
- self.runEspTool(command)
-
- def buttonFlash(self):
- param=self.runCheck()
- if not param:
- return
- if param['isFull']:
- command=['--chip','ESP32','--port',param['port'],'write_flash','0x1000',self.filename.get()]
- self.runEspTool(command)
- else:
- command=['--chip','ESP32','--port',param['port'],'erase_region','0xe000','0x2000']
- self.runEspTool(command)
- command = ['--chip', 'ESP32', '--port', param['port'], 'write_flash', '0x10000', self.filename.get()]
- self.runEspTool(command)
-
-
- def buttonCancel(self):
- self.interrupt=True
-
-
-if __name__ == "__main__":
- root = tk.Tk()
- app = App(root)
- root.mainloop()
diff --git a/tools/flashtool.pyz b/tools/flashtool.pyz
new file mode 100644
index 0000000..2f50de5
Binary files /dev/null and b/tools/flashtool.pyz differ
diff --git a/tools/esptool.py b/tools/flashtool/esptool.py
similarity index 100%
rename from tools/esptool.py
rename to tools/flashtool/esptool.py
diff --git a/tools/flashtool/flashtool.py b/tools/flashtool/flashtool.py
new file mode 100755
index 0000000..3a30162
--- /dev/null
+++ b/tools/flashtool/flashtool.py
@@ -0,0 +1,253 @@
+#! /usr/bin/env python3
+import subprocess
+import sys
+
+try:
+ import serial
+except ImportError:
+ subprocess.check_call([sys.executable, "-m", "pip", "install", 'pyserial'])
+finally:
+ import serial
+
+import tkinter as tk
+from tkinter import ttk
+import tkinter.font as tkFont
+
+import os
+import serial.tools.list_ports
+from tkinter import filedialog as FileDialog
+
+import builtins
+
+def main():
+ VERSION="Version 1.1, esptool 3.2"
+
+ oldprint=builtins.print
+ def print(*args, **kwargs):
+ app.addText(*args,**kwargs)
+
+
+ builtins.print=print
+ import esptool
+
+ class App:
+ def __init__(self, root):
+ root.title("ESP32 NMEA2000 Flash Tool")
+ root.geometry("800x600")
+ root.resizable(width=True, height=True)
+ root.configure(background='lightgrey')
+ root.columnconfigure(0, weight=1)
+ root.rowconfigure(0, weight=1)
+ frame=tk.Frame(root)
+ row=0
+ frame.grid(row=0,column=0,sticky='news',padx=10,pady=5)
+ DUMMY = "prevent to handled as virus"
+ #frame.configure(background='lightblue')
+ frame.columnconfigure(0,weight=1)
+ frame.columnconfigure(1, weight=3)
+ tk.Label(frame,text="ESP32 NMEA2000 Flash Tool").grid(row=row,column=0,columnspan=2,sticky='ew')
+ row+=1
+ tk.Label(frame, text=VERSION).grid(row=row,column=0,columnspan=2,sticky="ew",pady=10)
+ row+=1
+ self.mode=tk.IntVar()
+ self.mode.set(1)
+ rdFrame=tk.Frame(frame)
+ rdFrame.grid(row=row,column=1,sticky="ew",pady=20)
+ tk.Radiobutton(rdFrame,text="Initial Flash",value=1,variable=self.mode,command=self.changeMode).grid(row=0,column=0)
+ tk.Radiobutton(rdFrame, text="Update Flash", value=2, variable=self.mode,command=self.changeMode).grid(row=0,column=1)
+ row+=1
+ tk.Label(frame, text="Com Port").grid(row=row,column=0,sticky='ew')
+ ttk.Style().configure("TCombobox",padding=8,arrowsize=28)
+ ttk.Style().configure("TEntry", padding=8)
+ self.port=ttk.Combobox(frame)
+ self.port.grid(row=row,column=1,sticky="ew",pady=5)
+ row+=1
+ tk.Label(frame,text="Select Firmware").grid(row=row,column=0,sticky='ew')
+ self.filename=tk.StringVar()
+ fn=ttk.Entry(frame,textvariable=self.filename)
+ fn.grid(row=row,column=1,sticky='ew',pady=5)
+ fn.bind("<1>",self.fileDialog)
+ row+=1
+ self.fileInfo=tk.StringVar()
+ tk.Label(frame,textvariable=self.fileInfo).grid(row=row,column=0,columnspan=2,sticky="ew")
+ row+=1
+ self.flashInfo=tk.StringVar()
+ self.flashInfo.set("Address 0x1000")
+ tk.Label(frame,textvariable=self.flashInfo).grid(row=row,column=0,columnspan=2,sticky='ew',pady=10)
+ row+=1
+ btFrame=tk.Frame(frame)
+ btFrame.grid(row=row,column=0,columnspan=2,pady=15)
+ self.actionButtons=[]
+ bt=tk.Button(btFrame,text="Check",command=self.buttonCheck)
+ bt.grid(row=0,column=0)
+ self.actionButtons.append(bt)
+ bt=tk.Button(btFrame, text="Flash", command=self.buttonFlash)
+ bt.grid(row=0, column=1)
+ self.actionButtons.append(bt)
+ self.cancelButton=tk.Button(btFrame,text="Cancel",state=tk.DISABLED,command=self.buttonCancel)
+ self.cancelButton.grid(row=0,column=2)
+ row+=1
+ self.text_widget = tk.Text(frame)
+ frame.rowconfigure(row,weight=1)
+ self.text_widget.grid(row=row,column=0,columnspan=2,sticky='news')
+ self.readDevices()
+ self.interrupt=False
+
+ def updateFlashInfo(self):
+ if self.mode.get() == 1:
+ #full
+ self.flashInfo.set("Address 0x1000")
+ else:
+ self.flashInfo.set("Erase(otadata): 0xe000...0xffff, Address 0x10000")
+ def changeMode(self):
+ m=self.mode.get()
+ self.updateFlashInfo()
+ self.filename.set('')
+ self.fileInfo.set('')
+ def fileDialog(self,ev):
+ fn=FileDialog.askopenfilename()
+ if fn:
+ self.filename.set(fn)
+ info=self.checkImageFile(fn,self.mode.get() == 1)
+ if info['error']:
+ self.fileInfo.set("***ERROR: %s"%info['info'])
+ else:
+ self.fileInfo.set(info['info'])
+ def readDevices(self):
+ self.serialDevices=[]
+ names=[]
+ for dev in serial.tools.list_ports.comports(False):
+ self.serialDevices.append(dev.device)
+ if dev.description != 'n/a':
+ label=dev.description+"("+dev.device+")"
+ else:
+ label=dev.name+"("+dev.device+")"
+ names.append(label)
+ self.port.configure(values=names)
+ def addText(self,*args,**kwargs):
+ first=True
+ for k in args:
+ self.text_widget.insert(tk.END,k)
+ if not first:
+ self.text_widget.insert(tk.END, ',')
+ first=False
+ if kwargs.get('end') is None:
+ self.text_widget.insert(tk.END,"\n")
+ else:
+ self.text_widget.insert(tk.END,kwargs.get('end'))
+ self.text_widget.see('end')
+ root.update()
+ if self.interrupt:
+ self.interrupt=False
+ raise Exception("User cancel")
+
+ FULLOFFSET=61440
+ HDROFFSET = 288
+ VERSIONOFFSET = 16
+ NAMEOFFSET = 48
+ MINSIZE = HDROFFSET + NAMEOFFSET + 32
+ CHECKBYTES = {
+ 0: 0xe9, # image magic
+ 288: 0x32, # app header magic
+ 289: 0x54,
+ 290: 0xcd,
+ 291: 0xab
+ }
+
+ def getString(self,buffer, offset, len):
+ return buffer[offset:offset + len].rstrip(b'\0').decode('utf-8')
+
+ def getFirmwareInfo(self,ih,imageFile,offset):
+ buffer = ih.read(self.MINSIZE)
+ if len(buffer) != self.MINSIZE:
+ return self.setErr("invalid image file %s, to short"%imageFile)
+ for k, v in self.CHECKBYTES.items():
+ if buffer[k] != v:
+ return self.setErr("invalid magic at %d, expected %d got %d"
+ % (k+offset, v, buffer[k]))
+ name = self.getString(buffer, self.HDROFFSET + self.NAMEOFFSET, 32)
+ version = self.getString(buffer, self.HDROFFSET + self.VERSIONOFFSET, 32)
+ return {'error':False,'info':"%s:%s"%(name,version)}
+
+ def setErr(self,err):
+ return {'error':True,'info':err}
+ def checkImageFile(self,filename,isFull):
+ if not os.path.exists(filename):
+ return self.setErr("file %s not found"%filename)
+ with open(filename,"rb") as fh:
+ offset=0
+ if isFull:
+ b=fh.read(1)
+ if len(b) != 1:
+ return self.setErr("unable to read header")
+ if b[0] != 0xe9:
+ return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%b[0])
+ st=fh.seek(self.FULLOFFSET)
+ offset=self.FULLOFFSET
+ return self.getFirmwareInfo(fh,filename,offset)
+
+ def runCheck(self):
+ self.text_widget.delete("1.0", "end")
+ idx = self.port.current()
+ isFull = self.mode.get() == 1
+ if idx < 0:
+ self.addText("ERROR: no com port selected")
+ return
+ port = self.serialDevices[idx]
+ fn = self.filename.get()
+ if fn is None or fn == '':
+ self.addText("ERROR: no filename selected")
+ return
+ info = self.checkImageFile(fn, isFull)
+ if info['error']:
+ print("ERROR: %s" % info['info'])
+ return
+ return {'port':port,'isFull':isFull}
+
+ def runEspTool(self,command):
+ for b in self.actionButtons:
+ b.configure(state=tk.DISABLED)
+ self.cancelButton.configure(state=tk.NORMAL)
+ print("run esptool: %s" % " ".join(command))
+ root.update()
+ root.update_idletasks()
+ try:
+ esptool.main(command)
+ print("esptool done")
+ except Exception as e:
+ print("Exception in esptool %s" % e)
+ for b in self.actionButtons:
+ b.configure(state=tk.NORMAL)
+ self.cancelButton.configure(state=tk.DISABLED)
+ def buttonCheck(self):
+ param = self.runCheck()
+ if not param:
+ return
+ print("Settings OK")
+ command = ['--chip', 'ESP32', '--port', param['port'], 'chip_id']
+ self.runEspTool(command)
+
+ def buttonFlash(self):
+ param=self.runCheck()
+ if not param:
+ return
+ if param['isFull']:
+ command=['--chip','ESP32','--port',param['port'],'write_flash','0x1000',self.filename.get()]
+ self.runEspTool(command)
+ else:
+ command=['--chip','ESP32','--port',param['port'],'erase_region','0xe000','0x2000']
+ self.runEspTool(command)
+ command = ['--chip', 'ESP32', '--port', param['port'], 'write_flash', '0x10000', self.filename.get()]
+ self.runEspTool(command)
+
+
+ def buttonCancel(self):
+ self.interrupt=True
+
+ root = tk.Tk()
+ app = App(root)
+ root.mainloop()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/readme-esptool-win.txt b/tools/readme-esptool-win.txt
index 06bdfe0..4ff31ba 100644
--- a/tools/readme-esptool-win.txt
+++ b/tools/readme-esptool-win.txt
@@ -14,7 +14,7 @@ How to build the bundled flashtool.exe (on windows)
(2) pip install pyinstaller
(3) pip install pyserial
(4) in this directory:
- pyinstaller -F flashtool.py
+ pyinstaller --clean -F flashtool.py
will create flashtool.exe in dist
diff --git a/web/config.json b/web/config.json
index 3c3cb17..e2e45e8 100644
--- a/web/config.json
+++ b/web/config.json
@@ -376,7 +376,7 @@
"type": "number",
"default": "10110",
"description": "the TCP port we listen on",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "maxClients",
@@ -387,55 +387,128 @@
"min": 0,
"max": 6,
"description": "the number of clients we allow to connect to us",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "sendTCP",
- "label": "NMEA to TCP",
+ "label": "NMEA0183 out",
"type": "boolean",
"default": "true",
"description": "send out NMEA data to connected TCP clients",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "readTCP",
- "label": "NMEA from TCP",
+ "label": "NMEA0183 in",
"type": "boolean",
"default": "true",
"description": "receive NMEA data from connected TCP clients",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "tcpToN2k",
- "label": "TCP to NMEA2000",
+ "label": "to NMEA2000",
"type": "boolean",
"default": "true",
"description": "convert NMEA0183 from TCP clients to NMEA2000",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "tcpReadFilter",
- "label": "TCP read Filter",
+ "label": "NMEA read Filter",
"type": "filter",
"default": "",
"description": "filter for NMEA0183 data when reading from TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "tcpWriteFilter",
- "label": "TCP write Filter",
+ "label": "NMEA write Filter",
"type": "filter",
"default": "",
"description": "filter for NMEA0183 data when writing to TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
- "category": "TCP port"
+ "category": "TCP server"
},
{
"name": "sendSeasmart",
- "label": "Seasmart to TCP",
+ "label": "Seasmart out",
"type": "boolean",
"default": "false",
"description": "send NMEA2000 as seasmart to connected TCP clients",
- "category": "TCP port"
+ "category": "TCP server"
+ },
+ {
+ "name": "tclEnabled",
+ "label": "enable",
+ "type": "boolean",
+ "default": "false",
+ "description":"enable the TCP client",
+ "category":"TCP client"
+ },
+ {
+ "name": "remotePort",
+ "label": "remote port",
+ "type": "number",
+ "default": "10110",
+ "description": "the TCP port we connect to",
+ "category": "TCP client"
+ },
+ {
+ "name": "remoteAddress",
+ "label": "remote address",
+ "type": "string",
+ "default": "",
+ "check": "checkIpAddress",
+ "description": "the IP address we connect to in the form 192.168.1.2\nor an MDNS name like ESP32NMEA2K.local",
+ "category": "TCP client"
+ },
+ {
+ "name": "sendTCL",
+ "label": "NMEA0183 out",
+ "type": "boolean",
+ "default": "true",
+ "description": "send out NMEA data to remote TCP server",
+ "category": "TCP client"
+ },
+ {
+ "name": "readTCL",
+ "label": "NMEA0183 in",
+ "type": "boolean",
+ "default": "true",
+ "description": "receive NMEA data from remote TCP server",
+ "category": "TCP client"
+ },
+ {
+ "name": "tclToN2k",
+ "label": "to NMEA2000",
+ "type": "boolean",
+ "default": "true",
+ "description": "convert NMEA0183 from remote TCP server to NMEA2000",
+ "category": "TCP client"
+ },
+ {
+ "name": "tclReadFilter",
+ "label": "NMEA read Filter",
+ "type": "filter",
+ "default": "",
+ "description": "filter for NMEA0183 data when reading from remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
+ "category": "TCP client"
+ },
+ {
+ "name": "tclWriteFilter",
+ "label": "NMEA write Filter",
+ "type": "filter",
+ "default": "",
+ "description": "filter for NMEA0183 data when writing to remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
+ "category": "TCP client"
+ },
+ {
+ "name": "tclSeasmart",
+ "label": "Seasmart out",
+ "type": "boolean",
+ "default": "false",
+ "description": "send NMEA2000 as seasmart to remote TCP server",
+ "category": "TCP client"
},
{
"name": "wifiClient",
diff --git a/web/index.html b/web/index.html
index 8ae9a09..d6cc011 100644
--- a/web/index.html
+++ b/web/index.html
@@ -43,8 +43,16 @@
---
- # TCP clients
+ # clients
---
+
+
+ TCP client connected
+ ---
+
+
+ TCP client error
+ ---
diff --git a/web/index.js b/web/index.js
index 2634c74..eafb494 100644
--- a/web/index.js
+++ b/web/index.js
@@ -162,6 +162,14 @@ function checkAdminPass(v){
return checkApPass(v);
}
+function checkIpAddress(v,allValues,def){
+ if (allValues.tclEnabled != "true") return;
+ if (! v) return "cannot be empty";
+ if (! v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
+ && ! v.match(/.*\.local/))
+ return "must be either in the form 192.168.1.1 or xxx.local";
+}
+
function checkXDR(v,allValues){
if (! v) return;
let parts=v.split(',');
@@ -306,12 +314,14 @@ function updateMsgDetails(key, details) {
let counters={
count2Kin: 'NMEA2000 in',
count2Kout: 'NMEA2000 out',
- countTCPin: 'TCP in',
- countTCPout: 'TCP out',
+ countTCPin: 'TCPserver in',
+ countTCPout: 'TCPserver out',
+ countTCPClientin: 'TCPclient in',
+ countTCPClientout: 'TCPclient out',
countUSBin: 'USB in',
countUSBout: 'USB out',
- countSerialIn: 'Serial in',
- countSerialOut: 'Serial out'
+ countSERIn: 'Serial in',
+ countSEROut: 'Serial out'
}
function showOverlay(text, isHtml) {
let el = document.getElementById('overlayContent');
@@ -1360,7 +1370,9 @@ function sourceName(v){
if (v == 0) return "N2K";
if (v == 1) return "USB";
if (v == 2) return "SER";
- if (v >= 3) return "TCP";
+ if (v == 3) return "TCPcl"
+ if (v >= 4 && v <= 20) return "TCPser";
+ if (v >= 200) return "USER";
return "---";
}
let lastSelectList=[];