From bb51301383ea4ea270c31786518a4664790dec85 Mon Sep 17 00:00:00 2001 From: echicken <> Date: Mon, 30 Apr 2012 19:03:02 +0000 Subject: [PATCH] Support for KISS-mode TNCs and partial support for the AX.25 protocol. --- exec/load/kissAX25lib.js | 389 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 exec/load/kissAX25lib.js diff --git a/exec/load/kissAX25lib.js b/exec/load/kissAX25lib.js new file mode 100644 index 0000000000..2b72b70110 --- /dev/null +++ b/exec/load/kissAX25lib.js @@ -0,0 +1,389 @@ +// kissAX25lib.js for Synchronet 3.15+ +// echicken -at- bbs.electronicchicken.com (VE3XEC) +// Support for KISS TNCs and partial support for the AX.25 protocol. + +load("ax25defs.js"); + +// Return a kissTNC object (see below) based on 'section' of ctrl/kiss.ini +function loadKISSInterface(section) { + var f = new File(system.ctrl_dir + "kiss.ini"); + f.open("r"); + if(!f.exists || !f.is_open) return false; + var kissINI = f.iniGetObject(section); + f.close(); + var tnc = new kissTNC(section, kissINI.callsign, kissINI.ssid, kissINI.serialPort, Number(kissINI.baudRate)); + return tnc; +} + +// Load and configure all KISS TNCs, return an array of kissTNC objects (see below) +function loadKISSInterfaces() { + var f = new File(system.ctrl_dir + "kiss.ini"); + f.open("r"); + if(!f.exists || !f.is_open) return false; + var kissINI = f.iniGetAllObjects(); + f.close(); + var kissTNCs = new Array(); + for(var i = 0; i < kissINI.length; i++) kissTNCs.push(new kissTNC(kissINI[i].name, kissINI[i].callsign, kissINI[i].ssid, kissINI[i].serialPort, Number(kissINI[i].baudRate))); + return kissTNCs; +} + +// Create an object representing a KISS TNC, where object.handle is a COM object +function kissTNC(name, callsign, ssid, serialPort, baudRate) { + this.name = name; + this.callsign = callsign; + this.ssid = ssid; + this.handle = new COM(serialPort); + this.handle.baud_rate = parseInt(baudRate); + this.handle.open(); + if(!this.handle.is_open) return false; + + // Read a KISS frame from a TNC, return an AX.25 packet (array of bytes) less the flags and FCS + this.getKISSFrame = function() { + var escaped = false; + var kissByte = this.handle.readBin(2); + if(kissByte != (KISS_FEND<<8)) return false; + var kissFrame = new Array(); + // To do: add a timeout to this loop + while(kissByte != KISS_FEND) { + kissByte = this.handle.readBin(1); + if(kissByte == -1) continue; + if((kissByte & KISS_FESC) == KISS_FESC) { + escaped = true; + continue; + } + if(escaped && (kissByte & KISS_TFESC) == KISS_TFESC) { + kissFrame.push(KISS_FESC); + } else if(escaped && (kissByte & KISS_TFEND) == KISS_TFEND) { + kissFrame.push(KISS_FEND); + } else if(kissByte != KISS_FEND) { + kissFrame.push(kissByte); + } + escaped = false; + } + return kissFrame; + } + + // Write a KISS frame to a TNC where p is an AX.25 packet (array of bytes) less the flags and FCS + this.sendKISSFrame = function(p) { + var kissByte; + this.handle.writeBin((KISS_FEND<<8), 2); + for(var i = 0; i < p.length; i++) { + kissByte = p[i]; + if(kissByte == KISS_FEND) { + this.handle.writeBin((KISS_FESC<<8)|KISS_TFEND, 2); + } else if(kissByte == KISS_FESC) { + this.handle.writeBin((KISS_FESC<<8)|KISS_TFESC, 2); + } else { + this.handle.writeBin(kissByte, 1); + } + } + this.handle.writeBin(KISS_FEND, 1); + } +} + +function logByte(b) { + log(format("%d%d%d%d%d%d%d%d\r\n", (b & (1<<7)) ? 1 : 0, (b & (1<<6)) ? 1 : 0, (b & (1<<5)) ? 1 : 0, (b & (1<<4)) ? 1 : 0, (b & (1<<3)) ? 1 : 0, (b & (1<<2)) ? 1 : 0, (b & (1<<1)) ? 1 : 0, (b & (1<<0)) ? 1 : 0 )); +} + +function ax25packet() { + + this.assemble = function(destination, destinationSSID, source, sourceSSID, repeaters, control, pid, information) { + this.destination = destination; + this.destinationSSID = destinationSSID; + this.source = source; + this.sourceSSID = sourceSSID; + this.repeaters = repeaters; + this.control = control; + this.pid = pid; + this.information = information; + this.clientID = this.destination.replace(/\s/, "") + this.destinationSSID + this.source.replace(/\s/, "") + this.sourceSSID; + this.raw = new Array(); + var dest = stringToByteArray(this.destination); + for(var i = 0; i < dest.length; i++) this.raw.push((dest[i]<<1)); + this.raw.push((parseInt(this.destinationSSID)<<1)); + var src = stringToByteArray(this.source); + for(var i = 0; i < src.length; i++) this.raw.push((src[i]<<1)); + if(!repeaters) { + this.raw.push((parseInt(this.sourceSSID)<<1)|(1<<0)); + } else { + this.raw.push((parseInt(this.sourceSSID)<<1)); + for(var i = 0; i < this.repeaters.length; i++) { + var repeater = this.repeaters[i].split("-"); + var repeaterCall = stringToByteArray(repeater[0]); + for(var j = 0; j < repeaterCall.length; j++) this.raw.push((repeaterCall[j]<<1)); + var repeaterSSID = (parseInt(repeater[1])<<1); + if(i == this.repeaters.length - 1) repeaterSSID |= (1<<0); + this.raw.push(repeaterSSID); + } + } + this.raw.push(this.control); + if(this.pid !== undefined) this.raw.push(this.pid); + if(this.information !== undefined) { + for(var i = 0; i < this.information.length; i++) { + this.raw.push(this.information[i]); + } + this.ns = ((this.control & NS)>>>1); + } + if((this.control & I_FRAME) == I_FRAME || (this.control & S_FRAME) == S_FRAME) this.nr = ((this.control & NR)>>>5); + } + + this.disassemble = function(p) { + this.raw = p; + this.destination = ""; + for(var i = 0; i < 6; i++) this.destination += ascii((p[i]>>1)); + this.destinationSSID = ((p[6] & A_SSID)>>1); + this.source = ""; + for(var i = 7; i < 13; i++) this.source += ascii(p[i]>>1); + var i = 13; + this.sourceSSID = ((p[i] & A_SSID)>>1); + this.clientID = this.destination.replace(/\s/, "") + this.destinationSSID + this.source.replace(/\s/, "") + this.sourceSSID; + // Either the source callsign & SSID pair was the end of the address field, or we need to tack on a repeater path + if(p[i] & (1<<0)) { + this.repeaters = [0]; + } else { + var repeater = ""; + this.repeaters = new Array(); + for(var i = 14; i <= 78; i++) { + if(repeater.length == 6) { + repeater += "-" + ((p[i] & A_SSID)>>1); + this.repeaters.push(repeater); + repeater = ""; + } else { + repeater += ascii((p[i]>>1)); + } + if(p[i] & (1<<0)) break; + } + } + this.control = p[i + 1]; // Implementation can compare this against bitmasks from ax25defs.js to determine frame type. + // A U frame or an I frame will have a PID octet and an information field + if((this.control & U_FRAME_UI) == U_FRAME_UI || (this.control & U_FRAME_FRMR) == U_FRAME_FRMR || (this.control & I_FRAME) == I_FRAME) { + this.pid = p[i + 2]; + this.information = new Array(); + for(var x = i + 3; x < p.length; x++) this.information.push(p[x]); + } + // An I frame will have N(S) (send-sequence) bits in the control octet + if((this.control & I_FRAME) == I_FRAME) this.ns = ((this.control & NS)>>>1); + // An I frame or an S frame will have N(R) (receive sequence) bits in the control octet + if((this.control & I_FRAME) == I_FRAME || (this.control & S_FRAME) == S_FRAME) this.nr = ((this.control & NR)>>>5); + } + + this.logPacket = function() { + var x = "Unknown or unhandled frame type"; + if((this.control & U_FRAME_SABM) == U_FRAME_SABM) { + x = "U_FRAME_SABM"; + } else if((this.control & U_FRAME_UA) == U_FRAME_UA) { + x = "U_FRAME_UA"; + } else if((this.control & U_FRAME_FRMR) == U_FRAME_FRMR) { + x = "U_FRAME_FRMR"; + } else if((this.control & U_FRAME_DISC) == U_FRAME_DISC) { + x = "U_FRAME_DISC"; + } else if((this.control & U_FRAME) == U_FRAME) { + x = "U_FRAME"; + } else if((this.control & S_FRAME_REJ) == S_FRAME_REJ) { + x = "S_FRAME_REJ, N(R): " + this.nr; + } else if((this.control & S_FRAME_RNR) == S_FRAME_RNR) { + x = "S_FRAME_RNR, N(R): " + this.nr; + } else if((this.control & S_FRAME) == S_FRAME) { + x = "S_FRAME, N(R): " + this.nr; + } else if((this.control & I_FRAME) == I_FRAME) { + x = "I_FRAME, N(R): " + this.nr + ", N(S): " + this.ns; + } + log(LOG_DEBUG, this.source + "-" + this.sourceSSID + "->" + this.destination + "-" + this.destinationSSID + ": " + x); + } +} + +// Turns a string into an array of ASCII character codes +function stringToByteArray(s) { + s = s.split(""); + var r = new Array(); + for(var i = 0; i < s.length; i++) r.push(ascii(s[i])); + return r; +} + +// Turns an array of ASCII character codes into a string +function byteArrayToString(s) { + var r = ""; + for(var i = 0; i < s.length; i++) r += ascii(s[i]); + return r; +} + +// Create an AX.25 client object from an ax25packet() object +function ax25Client(destination, destinationSSID, source, sourceSSID, k) { + this.kissTNC = k; + if(source == k.callsign && sourceSSID == k.ssid) { + this.callsign = destination; + this.ssid = destinationSSID; + } else { + this.callsign = source; + this.ssid = sourceSSID; + } + this.clientID = destination.replace(/\s/, "") + destinationSSID + source.replace(/\s/, "") + sourceSSID; + this.ssv = 0; // Send Sequence Variable + this.rsv = 0; // Receive Sequence Variable + this.ns = 0; // Client's last reported N(S) + this.nr = 0; // Client's last reported N(R) + this.t1 = 0; // Timer T1 + this.t2 = 0; // Timer T2 + this.t3 = 0; // Timer T3 + this.resend = false; + this.reject = false; + this.connected = false; + this.sentIFrames = []; + this.lastPacket = false; + + /* Process and respond to ax25packet object 'p', returning false unless + 'p' is an I frame, in which case the I frame payload will be returned + as an array of bytes. + + Argument 'p' is optional; if it is not supplied, this function will + try to read an AX.25 frame from the KISS interface associated with + this client. */ + this.receive = function(p) { + if(p == undefined) { + var kissFrame = this.kissTNC.getKISSFrame(); + if(!kissFrame) return false; + var p = new ax25packet(); + p.disassemble(kissFrame); + if(p.destination != this.kissTNC.callsign || p.destinationSSID != this.kissTNC.ssid) return false; + } + p.logPacket(); + var retval = false; + var a = new ax25packet; + if((p.control & U_FRAME_SABM) == U_FRAME_SABM) { + this.connected = true; + if(this.reject) { + this.reject = false; + } else { + this.rsv = 0; + this.ssv = 0; + } + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_UA); + log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + ": Connection from " + this.callsign + "-" + this.ssid); + } else if((p.control & U_FRAME_DM) == U_FRAME_DM) { + this.connected = false; + } else if((p.control & U_FRAME_UA) == U_FRAME_UA) { + if((this.lastPacket.control & U_FRAME_SABM) == U_FRAME_SABM) this.connected = true; + if((this.lastPacket.control & U_FRAME_DISC) == U_FRAME_DISC) this.connected = false; + return retval; + } else if((p.control & U_FRAME_FRMR) == U_FRAME_FRMR && this.connected) { + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM); + this.rsv = 0; + this.ssv = 0; + } else if((p.control & U_FRAME_DISC) == U_FRAME_DISC) { + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_UA); + this.connected = false; + this.reject = false; + } else if((p.control & U_FRAME) == U_FRAME) { + // Unnumbered Information fields should be processed, I guess. + return retval; + } else if(!this.connected) { + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_DM); + } else if((p.control & S_FRAME_REJ) == S_FRAME_REJ) { + this.resend = true; + a.raw = this.sentIFrames[p.nr - 1]; + this.nr = p.nr; + } else if((p.control & S_FRAME_RNR) == S_FRAME_RNR) { + this.nr = p.nr; + } else if((p.control & S_FRAME) == S_FRAME) { + // This is a Receive-Ready and an acknowledgement of all frames in the sequence up to client's N(R) + this.nr = p.nr; + if(p.nr == 7 && this.sentIFrames.length == 7) { + this.sentIFrames = []; // Client acknowledges the entire sequence, we can ditch our stored sent packets + return retval; + } else if(p.nr == 7 && this.sentIFrames.length > 7) { + this.sentIFrames = this.sentIFrames.slice(7); // If we sent more I frames before they acknowledged our N(S)=7, we don't want to delete them yet. + return retval; + } else if(this.resend && p.nr < this.sentIFrames.length) { + a.raw = this.sentIFrames[p.nr - 1]; + } else if(this.resend && p.nr >= this.sentIFrames.length) { + this.resend = false; + return retval; + } else { + return retval; + } + } else if((p.control & I_FRAME) == I_FRAME) { + this.ns = p.ns; + this.nr = p.nr; + if(p.ns != this.rsv) { + if(this.reject) return retval; + // Send a REJ, requesting retransmission of the frame whose N(S) value matches our current RSV + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (S_FRAME_REJ|(this.rsv<<5))); + this.reject = true; + } else if(p.information.length <= 256) { + // This is an actual, good and expected I frame + this.rsv++; + if(this.rsv > 7) this.rsv = 0; + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (S_FRAME_RR|(this.rsv<<5))); + if(p.hasOwnProperty("information") || p.information.length > 0) retval = p.information; + this.reject = false; + } else { + // Send a FRMR with the offending control field, our RSV and SSV, and the "Z" flag to indicate an invalid N(R) + var i = [p.control, (this.rsv<<5)|(this.ssv<<1), 0]; + if(p.information.length > 256) i[2] = (1<<2); + if(p.nr != this.ssv) i[2]|=(1<<3); + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (U_FRAME_FRMR), PID_NONE, i); + this.reject = true; + } + } else { + return retval; + } + this.sendPacket(a); + return retval; + } + + // Send ax25Packet object 'a' to an ax25Client + this.sendPacket = function(a) { + this.lastPacket = a; + this.kissTNC.sendKISSFrame(a.raw); + a.logPacket(); + } + + // Send an I Frame to an ax25Client, with payload 'p' + this.send = function(p) { + var a = new ax25packet(); + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (I_FRAME|(this.rsv<<5)|(this.ssv<<1)), PID_NONE, p); + this.sendPacket(a); + this.ssv++; + if(this.ssv > 7) this.ssv = 0; + this.sentIFrames.push(a); + } + + // Connect this client to an AX.25 host, 5 attempts at 3 second intervals + // (Attempts and intervals should probably be made configurable) + this.connect = function() { + var a = new ax25packet(); + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM); + var i = 0; + while(!c.connected && i < 5){ + this.sendPacket(a); + mswait(3000); + this.receive(); + i++; + } + if(c.connected) { + log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + " connected to " + this.callsign + "-" + this.ssid); + } else { + log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + " failed to connect to " + this.callsign + "-" + this.ssid); + } + } + + // Disconnect this client, 5 attempts at 3 second intervals + // (Attempts and intervals should probably be made configurable) + this.disconnect = function() { + var a = new ax25packet(); + a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_DISC); + var i = 0; + while(c.connected && i < 5){ + this.sendPacket(a); + mswait(3000); + this.receive(); + i++; + } + if(c.connected) { + c.connected = false; + log(LOG_INFO, this.callsign + "-" + this.ssid + " failed to acknowledge U_FRAME_DISC from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid); + } else { + log(LOG_INFO, this.callsign + "-" + this.ssid + " disconnected from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid); + } + } +} -- GitLab