diff --git a/exec/load/ax25defs.js b/exec/load/ax25defs.js index ea413f21a26157a8a1a8b0e736ae6198e9d1e970..3f073afc2e69b75a748956e19b4b3b7b4a51d87f 100644 --- a/exec/load/ax25defs.js +++ b/exec/load/ax25defs.js @@ -4,58 +4,63 @@ const AX25_FLAG = (1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6); // Unused, but included for non-KISS implementations. // Address field - SSID subfield bitmasks -const A_CRH = (1<<7); // Command/Response or Has-Been-Repeated bit of an SSID octet -const A_RR = (1<<5)|(1<<6); // The "R" (reserved) bits of an SSID octet -const A_SSID = (1<<1)|(1<<2)|(1<<3)|(1<<4); // The SSID portion of an SSID octet +const A_CRH = (1<<7); // Command/Response or Has-Been-Repeated bit of an SSID octet +const A_RR = (1<<5)|(1<<6); // The "R" (reserved) bits of an SSID octet +const A_SSID = (1<<1)|(1<<2)|(1<<3)|(1<<4); // The SSID portion of an SSID octet // Control field bitmasks -const PF = (1<<4); // Poll/Final +const PF = (1<<4); // Poll/Final const NR = (1<<5)|(1<<6)|(1<<7); // N(R) - receive sequence number const NS = (1<<1)|(1<<2)|(1<<3); // N(S) - send sequence number // Information frame -const I_FRAME = 0; // Derp +const I_FRAME = 0; +const I_FRAME_MASK = 1; // Supervisory frame and subtypes -const S_FRAME = (1<<0); -const S_FRAME_RR = S_FRAME; // Receive Ready -const S_FRAME_RNR = S_FRAME|(1<<2); // Receive Not Ready -const S_FRAME_REJ = S_FRAME|(1<<3); // Reject +const S_FRAME = 1; +const S_FRAME_RR = S_FRAME; // Receive Ready +const S_FRAME_RNR = S_FRAME|(1<<2); // Receive Not Ready +const S_FRAME_REJ = S_FRAME|(1<<3); // Reject +const S_FRAME_MASK = S_FRAME|S_FRAME_RR|S_FRAME_RNR|S_FRAME_REJ // Unnumbered frame and subtypes -const U_FRAME = (1<<0)|(1<<1); -const U_FRAME_SABM = U_FRAME|(1<<2)|(1<<3)|(1<<5); // Set Asynchronous Balanced Mode -const U_FRAME_DISC = U_FRAME|(1<<6); // Disconnect -const U_FRAME_DM = U_FRAME|(1<<2)|(1<<3); // Disconnected Mode -const U_FRAME_UA = U_FRAME|(1<<5)|(1<<6); // Acknowledge -const U_FRAME_FRMR = U_FRAME|(1<<2)|(1<<7); // Frame Reject -const U_FRAME_UI = U_FRAME; // Information +const U_FRAME = 3; +const U_FRAME_SABM = U_FRAME|(1<<2)|(1<<3)|(1<<5); // Set Asynchronous Balanced Mode +const U_FRAME_DISC = U_FRAME|(1<<6); // Disconnect +const U_FRAME_DM = U_FRAME|(1<<2)|(1<<3); // Disconnected Mode +const U_FRAME_UA = U_FRAME|(1<<5)|(1<<6); // Acknowledge +const U_FRAME_FRMR = U_FRAME|(1<<2)|(1<<7); // Frame Reject +const U_FRAME_UI = U_FRAME; // Information +const U_FRAME_MASK = U_FRAME|U_FRAME_SABM|U_FRAME_DISC|U_FRAME_DM|U_FRAME_UA|U_FRAME_FRMR; // Protocol ID field bitmasks (most are unlikely to be used, but are here for the sake of completeness.) -const PID_X25 = (1<<0); // ISO 8208/CCITT X.25 PLP -const PID_CTCPIP = (1<<1)|(1<<2); // Compressed TCP/IP packet. Van Jacobson (RFC 1144) -const PID_UCTCPIP = (1<<0)|(1<<1)|(1<<2); // Uncompressed TCP/IP packet. Van Jacobson (RFC 1144) -const PID_SEGF = (1<<4); // Segmentation fragment -const PID_TEXNET = (1<<0)|(1<<1)|(1<<6)|(1<<7); // TEXNET datagram protocol -const PID_LQP = (1<<2)|(1<<6)|(1<<7); // Link Quality Protocol -const PID_ATALK = (1<<1)|(1<<3)|(1<<6)|(1<<7); // Appletalk -const PID_ATALKARP = (1<<0)|(1<<1)|(1<<3)|(1<<6)|(1<<7); // Appletalk ARP -const PID_ARPAIP = (1<<2)|(1<<3)|(1<<6)|(1<<7); // ARPA Internet Protocol -const PID_ARPAAR = (1<<0)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // ARPA Address Resolution -const PID_FLEXNET = (1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // FlexNet -const PID_NETROM = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // Net/ROM -const PID_NONE = (1<<4)|(1<<5)|(1<<6)|(1<<7); // No layer 3 protocol implemented -const PID_ESC = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7); // Escape character. Next octet contains more Level 3 protocol information. +const PID_X25 = 1; // ISO 8208/CCITT X.25 PLP +const PID_CTCPIP = (1<<1)|(1<<2); // Compressed TCP/IP packet. Van Jacobson (RFC 1144) +const PID_UCTCPIP = (1<<0)|(1<<1)|(1<<2); // Uncompressed TCP/IP packet. Van Jacobson (RFC 1144) +const PID_SEGF = (1<<4); // Segmentation fragment +const PID_TEXNET = (1<<0)|(1<<1)|(1<<6)|(1<<7); // TEXNET datagram protocol +const PID_LQP = (1<<2)|(1<<6)|(1<<7); // Link Quality Protocol +const PID_ATALK = (1<<1)|(1<<3)|(1<<6)|(1<<7); // Appletalk +const PID_ATALKARP = (1<<0)|(1<<1)|(1<<3)|(1<<6)|(1<<7); // Appletalk ARP +const PID_ARPAIP = (1<<2)|(1<<3)|(1<<6)|(1<<7); // ARPA Internet Protocol +const PID_ARPAAR = (1<<0)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // ARPA Address Resolution +const PID_FLEXNET = (1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // FlexNet +const PID_NETROM = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7); // Net/ROM +const PID_NONE = (1<<4)|(1<<5)|(1<<6)|(1<<7); // No layer 3 protocol implemented +const PID_ESC = 255; // Escape character. Next octet contains more Level 3 protocol information. // KISS protocol-related constants // FEND and transpositions -const KISS_FEND = (1<<6)|(1<<7); // Frame end -const KISS_FESC = (1<<0)|(1<<1)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Frame escape -const KISS_TFEND = (1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Transposed frame end -const KISS_TFESC = (1<<0)|(1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Transposed frame escape +const KISS_FEND = (1<<6)|(1<<7); // Frame end +const KISS_FESC = (1<<0)|(1<<1)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Frame escape +const KISS_TFEND = (1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Transposed frame end +const KISS_TFESC = (1<<0)|(1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7); // Transposed frame escape -// Commands (SetHardware (0x06) excluded intentionally.) -const KISS_DF = 0; // Data frame -const KISS_TXD = (1<<0); // TX delay -const KISS_P = (1<<1); // Persistence -const KISS_ST = (1<<0)|(1<<1); // Slot time -const KISS_TXT = (1<<2); // TX tail -const KISS_FD = (1<<0)|(1<<2); // Full Duplex +// Commands +const KISS_DATAFRAME = 0; // Data frame +const KISS_TXDELAY = 1; // TX delay +const KISS_PERSISTENCE = 2; // Persistence +const KISS_SLOTTIME = 3; // Slot time +const KISS_TXTAIL = 4; // TX tail +const KISS_FULLDUPLEX = 5; // Full Duplex +const KISS_SETHARDWARE = 6; // Set Hardware +const KISS_RETURN = 255; // Exit KISS mode \ No newline at end of file diff --git a/exec/load/kissAX25lib.js b/exec/load/kissAX25lib.js index f5506be09e6869104c4a6eb69813707a63232e5f..cc972f5495d5af86acf827a6b116f0efabd57f8f 100644 --- a/exec/load/kissAX25lib.js +++ b/exec/load/kissAX25lib.js @@ -1,674 +1,1826 @@ -/* kissAX25lib.js for Synchronet 3.15+ - echicken -at- bbs.electronicchicken.com (VE3XEC) +/* kissAX25lib.js for Synchronet 3.16+ + VE3XEC (echicken -at- bbs.electronicchicken.com) - This library provides support for the KISS and AX.25 protocols. AX.25 - support is partial and needs much improvement. +This library adds support for the KISS and AX.25 protocols to Synchronet BBS. +You can find the latest copy on the Synchronet CVS at cvs.synchro.net. - The following functions are available: +Dependencies: - loadKISSInterface(section) - - 'section' is the name of a section from ctrl/kiss.ini - - Returns a kissTNC object - - loadKISSInterfaces() - - Takes no arguments and loads all interfaces defined in ctrl/kiss.ini - - Returns an array of kissTNC objects - - logbyte(b) - - Adds a representation of an eight-bit byte to the log (eg. 00111100) - - Occasionally useful for debugging - - stringToByteArray(s) - - Converts string 's' into an array of ASCII values - - Returns an array with one element per character in 's' - - byteArrayToString(s) - - Converts byte array 's' (as above) into a string and returns it - - The following objects are available: + exec/load/ax25defs.js - Masks for bitwise operations + ctrl/kiss.ini - This is where your TNCs are defined and configured - kissTNC(name, callsign, ssid, serialPort, baudRate) - - Normally you'll use loadKISSInterface(section) to instantiate these, - however the arguments it accepts are obvious if you want to create - instances manually and bypass kiss.ini. - - Properties: - - 'name' - A textual name for the TNC (a section heading from kiss.ini) - - 'callsign' - The callsign assigned to this TNC - - 'ssid' - The SSID assigned to this TNC - - 'handle' - A COM object representing the serial connection to the TNC. This - has its own methods for reading and writing data to and from the - TNC. - - Methods: - - getKISSFrame() - A bit of a misnomer. What it actually does is read a KISS frame - from the TNC and then returns an array representing the bytes of - the AX.25 packet contained therein. - - sendKISSFrame(p) - Where 'p' is array containing the bytes of an AX.25 frame, this - will encapsulate that packet in a KISS frame and then send it to - the TNC for transmission. - - beacon() - Causes the TNC to transmit a UI frame containing the value of - system.name (the name of your BBS.) I'll probably make the beacon - text configurable in kiss.ini later on. - - ax25packet() +Objects, Properties, and Methods: - Represents a single AX.25 frame, but is mostly devoid of properties - until either the assemble() or disassemble() methods are called. - - assemble(destination, destinationSSID, source, sourceSSID, repeaters, control, pid, information) - - 'repeaters' is the digipeater path, an array of 'callsign-ssid' - - 'control' is a control field octet from ax25defs.js, eg. U_FRAME - - 'pid' is a protocol ID octet from ax25defs.js (usually PID_NONE) - - 'information' is the payload of an I or UI frame, eg. the return - value of stringToByteArray(s) - - disassemble(p) - 'p' is an array containing the bytes of an AX.25 frame, eg. the - return value of kissTNC.getKISSFrame(). - - logPacket() - Writes some information about this packet to the log. - - After assemble() or disassemble() has been called, your ax25packet - object will have the following properties: - - 'destination' - The remote callsign - - 'destinationSSID' - The remote SSID - - 'source' - The local callsign - - 'sourceSSID' - The local SSID - - 'repeaters' - An array of 'callsign-ssid' representing the digipeater path - - 'control' - The control octet, which can be compared bitwise against masks - defined in ax25defs.js - - 'pid' - The protocol ID octet, which can be compared bitwise against - masks defined in ax25defs.js - - 'information' - The payload if applicable (I or UI frames only,) good for use - with byteArrayToString(s) - - 'clientID' - A unique ID for the sender or recipient, for internal use - - 'raw' - An array representing the bytes of the frame, good for use - with kissTNC.sendKISSFrame(p) - - ax25Client(destination, destinationSSID, source, sourceSSID, k) - - Can represent a client that has called you, but can also be used to - represent an outgoing session that you have initiated. - - All arguments but 'k' are self-explanatory. 'k' is a reference to a - kissTNC object (see above) and will be the interface through which all - traffic for this client shall pass. - - Properties: - - 'kissTNC' - A reference to the kissTNC object representing the TNC through - which all traffic to and from this client passes - - 'callsign' - The local callsign - - 'ssid' - The local SSID - - 'clientID' - A unique ID for this client, for internal use, same as - ax25packet.clientID - - 'ssv' - The send-sequence variable - - 'rsv' - The receive-sequence variable - - 'ns' - Client's last reported N(S) - - 'nr' - Client's last reported N(R) - - 't1' - Timer T1 (Placeholder; not yet implemented) +AX25 (Object) + + Contains settings that should be global to your entire application. Is also + the parent of all other KISS & AX.25 related objects. + + Properties: + + logging - Set to 'true' to log all packets in and out of all TNCs + (This logging will be done at the DEBUG log level.) + tncs - Object containing references to every loaded AX25.KISSTNC object + clients - Object containing references to every loaded AX25.Client object + + Methods: + + loadTNC(tncName) + + Where 'tncName' is a section name from ctrl/kiss.ini, returns an + AX25.KISSTNC object, and adds that object to AX25.tncs + + AX25.loadAllTNCs() - 't2' - Timer T2 (Placeholder; not yet implemented) + Populates AX25.tncs with an object for each section in ctrl/kiss.ini. + +AX25.KISSTNC(serialPort, baudRate, callsign, ssid) + + Creates a new AX25.KISSTNC object, eg: + var k = new AX25.KISSTNC("COM1", 9600, "MYCALL", 1); + A reference to this TNC will be placed in AX25.tncs. + + Arguments: + + serialPort - Device name; "COM1", "/dev/ttyUSB0", etc. + baudRate - The speed of the serial connection between your computer + and your TNC. 1200, 9600, etc. + callsign - Alphanumerics only + ssid - A number between 0 and 15 + name - Whatver you want; usually a section name from kiss.ini + + Settings: + + serialPort - As above + baudRate - As above + callsign - As above + ssid - As above + timeout - Timeout when reading a packet from the TNC, in milliseconds + txDelay - The delay between the time when the TNC keys down the + transmitter and when it sends audio, in milliseconds + txTail - The delay between the time when the TNC stops sending + audio and when it unkeys the transmitter, in milliseconds + persistence - CSMA persistence, between 0 and 1 (default: 0.25) + slotTime - CSMA slot time, in milliseconds + fullDuplex - true/false (default: false) + + Properties: + + dataWaiting - true if a packet is waiting to be read + dataPending - true if a packet is waiting to be sent + + Methods: + + setHardware(command) - Set a TNC-specific configuration option, where + 'command' is a number from 0 to 255. Refer to + your TNC documentation for valid 'command' values + + exitKISS() - Take the TNC out of KISS mode + + receive() - Returns an AX25.Packet object, or false if there + are no packets waiting in the receive buffer + + send(packet) - Adds AX25.Packet object 'packet' to the outgoing + packet queue + + cycle() - Appends to the receive buffer any packets waiting + to be read from the interface. Sends to the + interface any packets waiting in the outgoing + queue. + +AX25.Packet(frame) - 't3' - Timer T3 (Placeholder; not yet implemented) + Creates a new AX25.Packet object. - 'resend' - true/false, for internal use - - 'reject' - true/false, for internal use - - 'wait' - If true, do not send any additional I frames to this client. (Your - scripts should check this value before deciding to send data to - the client.) - - 'connected' - true/false, whether or not this client is connected + Arguments: + + frame - Optional, and normally only supplied by kissTNC.getPacket(), + which returns an AX25.Packet object to you. + + Properties: + + destinationCallsign - The callsign of the destination station (<= 6 chars) + destinationSSID - The SSID of the destination station (0 - 15) + sourceCallsign - The callsign of the source station (<= 6 chars) + sourceSSID - The SSID of the source station (0 - 15) + repeaterPath - An array of { callsign, ssid } objects + pollFinal - True if the poll/final bit is set + command - True if this is a command + response - True if this is a response + control - The control field (frame type with N(R), N(S), P/F + set as applicable.) This property is read only. If + you want to modify it, modify its constituent parts: + .type, .nr, .ns, .pollFinal + type - The control field with N(R), N(S), P/F left unset + (Will match one of the frame types defined in + ax25defs.js, eg. U_FRAME.) + nr - The receive sequence number of an I or S frame + ns - The send sequence number of an I frame + pid - The PID field of an I or UI frame + info - The info field of an I or UI frame + clientID - If there was a client associated with this packet, + this is what its ID would be. + + Methods: + + These methods are normally called indirectly by the kissTNC object's + getPacket() and sendPacket() methods. + + disassemble(frame) - Where frame is an array of the bytes of a raw AX.25 + frame (less the start/stop flags and FCS), populates + the above properties of this instance of the object + with data from that frame. (If 'frame' is supplied + when creating an AX25.Packet object, this method will + be called automatically.) + + assemble() - Opposite of disassemble; returns an array of bytes + of a raw AX.25 frame (less the start/stop flags and + FCS) based on the properties of this instance of the + object. + +AX25.Client(tnc, packet) - 'sentIFrames' - An array of the last seven I frames that we have sent to this - client. + Where 'tnc' is the AX25.KISSTNC object associated with this client, and the + optional 'packet' argument is a *received* AX25.Packet object. - 'lastPacket' - An ax25packet object representing the last packet that we sent to - this client. - - Methods: - - receive(p) - 'p' is an optional ax25packet object; if omitted, an attempt will - be made to read an AX.25 frame from the KISS TNC associated with - this client. Returns false if there is no AX.25 frame waiting to - be received. - - sendPacket(a) - 'a' is an ax25packet object that will be sent to the client via - its associated KISS TNC. - - send(p) - Sends an I frame to the client, where 'p' is the payload (eg. a - return value from stringToByteArray(s). - - connect() - Makes five attempts to connect to the client, at three second - intervals. (Attempts and intervals to be made configurable at - some point.) To verify the connection state afterward, check - the value of ax25client.connected. - - disconnect() - Makes five attempts to disconnect the client, at three second - intervals. (Attempts and intervals to be made configurable at - some point. To verify the connection state afterward, check the - value of ax25client.connected. - -*/ + Public Properties: + + callsign - The callsign of the remote station + ssid - The SSID of the remote station + id - Unique ID for this client (can be compared against + the 'clientID' property of an inbound packet to + match an AX25.Packet with an existing AX25.Client. + connected - true if the link is active + dataWaiting - true if data is waiting in the receive buffer + + Public Methods: + + connect(callsign, ssid) - Connect to station <callsign>-<ssid>, SSID will + default to 0 if omitted. + disconnect() - Terminate the link. + receivePacket(packet) - Add 'packet' (ie. a packet object read from an + AX25.KISSTNC object) to the incoming packet buffer + to be processed (and responded to) the next time + .cycle() is called. + send(array) - Add 'array' to the send buffer. Data from this + buffer will be sent out in sequence when .cycle() + is called. + sendString(string) - Converts 'string' to an array of ASCII codes, then + places that array in the send buffer to be sent + out the next time .cycle() is called. + receive() - Returns an array of eight bit binary integers, or + false if no data is waiting to be read. + receiveString() - Treats the next array of bytes in the receive + buffer as an array of ASCII codes, converts that + array into a string and returns it. + cycle() - Sends out any pending data up to the maximum + amount possible. Processes any packets in the + incoming packet buffer, populating the receive + buffer with data as applicable. */ load("ax25defs.js"); -// Return a kissTNC object (see below) based on 'section' of ctrl/kiss.ini -function loadKISSInterface(section) { +function logByte(b) { + log( + format( + "%d%d%d%d%d%d%d%d", + (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 + ) + ); +} + +/* distanceBetween(leader, follower, modulus) + Find the difference between 'leader' and 'follower' modulo 'modulus'. */ +var distanceBetween = function(l, f, m) { + return (l < f) ? l + (m - f) : l - f; +} + +/* wrapAround(number, modulus) + If 'number' happens to be negative, it will be wrapped back around 'modulus' + eg. wrapAround(-1, 8) == 7. */ +var wrapAround = function(n, m) { + return (n < 0) ? m - Math.abs(n) : n; +} + +/* testCallsign(callsign) - boolean + Returns true if 'callsign' is a valid AX.25 callsign (a string + containing up to six letters and numbers only.) */ +var testCallsign = function(callsign) { + if(typeof callsign == "undefined" || callsign.length > 6) + return false; + callsign = callsign.toUpperCase().replace(/\s*$/g, ""); + for(var c = 0; c < callsign.length; c++) { + var a = ascii(callsign[c]); + if( + (a >= 48 && a <= 57) + || + (a >=65 && a <=90) + ) { + continue; + } + return false; + } + return true; +} + +// 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; +} + +var AX25 = { + + 'logging' : false,// Set true to log packets in and out of TNCs + 'tncs' : {}, // Indexed by AX25.KISSTNC.id + 'clients' : {}, // Indexed by AX25.Client.id/AX25.Packet.clientID + + // I plan to make this setting more configurable at some point. + // Flag + Destination + Source + Repeaters + Control + PID + Info + Flag + 'maximumPacketSize' : 8 + 56 + 56 + 448 + 8 + 8 + 2048 + 8 // 2640 Bits + +}; + +AX25.loadTNC = function(tncName) { 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); + var tncIni = f.iniGetObject(tncName); f.close(); - var tnc = new kissTNC(section, kissINI.callsign, kissINI.ssid, kissINI.serialPort, Number(kissINI.baudRate)); + if(tncIni == null) { + throw format( + "AX25: KISS TNC %s not found in %skiss.ini", + tncName, system.ctrl_dir + ); + } + var tnc = new AX25.KISSTNC( + tncIni.serialPort, + tncIni.baudRate, + tncIni.callsign, + tncIni.ssid + ); + for(var property in tncIni) { + if( + property == "serialPort" + || + property == "baudRate" + || + property == "callsign" + || + property == "ssid" + ) { + continue; + } + if(tnc.hasOwnProperty(property)) + tnc[property] = tncIni[property]; + else + throw "AX25: Unknown TNC property " + property; + } return tnc; } -// Load and configure all KISS TNCs, return an array of kissTNC objects (see below) -function loadKISSInterfaces() { +AX25.loadAllTNCs = function() { var f = new File(system.ctrl_dir + "kiss.ini"); f.open("r"); - if(!f.exists || !f.is_open) - return false; - var kissINI = f.iniGetAllObjects(); + var sections = f.iniGetSections(); 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))); + if(typeof sections == "undefined" || sections == null) { + throw format( + "AX.25: Unable to read TNC configuration from %skiss.ini", + system.ctrl_dir + ); } - return kissTNCs; + for(var s = 0; s < sections.length; s++) + AX25.loadTNC(sections[s]); } -// 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; +AX25.KISSTNC = function(serialPort, baudRate, callsign, ssid) { + + var settings = { + 'serialPort' : "", + 'baudRate' : 0, + 'callsign' : "", + 'ssid' : 0, + 'port' : 0, + 'timeout' : 10000, + 'txDelay' : 500, + 'persistence' : 63, + 'slotTime' : 100, + 'txTail' : 0, + 'fullDuplex' : false + }; + + var buffers = { + 'send' : [], + 'receive' : [] + }; + + var com = new COM(""); + + var sendCommand = function(command, value) { + com.sendBin(KISS_FEND, 1); + com.sendBin((((settings.port<<4)|command)<<8)|value, 2); + com.sendBin(KISS_FEND, 1); + } + + this.__defineGetter__( + "serialPort", + function() { + return settings.serialPort; + } + ); + + this.__defineSetter__( + "serialPort", + function(serialPort) { + if(com.is_open) + com.close(); + if(typeof serialPort == "undefined") + throw "Unable to open nonexistent serial port."; + com = new COM(serialPort); + com.open(); + if(!com.is_open) { + throw format( + "Error opening serial port '%s': %s.", + serialPort, + com.last_error + ); + } + } + ); + + this.__defineGetter__( + "baudRate", + function() { + return settings.baudRate; + } + ); + + this.__defineSetter__( + "baudRate", + function(baudRate) { + if(typeof baudRate == "undefined" || isNaN(Math.abs(baudRate))) { + throw format( + "Invalid baud rate: %s.", + baudRate + ); + } else { + settings.baudRate = Math.abs(baudRate); + if(com.is_open) + com.close(); + com.baud_rate = settings.baudRate; + com.open(); + if(!com.is_open) { + throw format( + "Error setting baud rate '%s': %s", + baudRate, + com.last_error + ); + } + } + } + ); + + this.__defineGetter__( + "callsign", + function() { + return callsign; + } + ); + + this.__defineSetter__( + "callsign", + function(callsign) { + if(typeof callsign == "undefined" || !testCallsign(callsign)) { + throw format( + "Invalid callsign: %s.", + callsign + ); + } else { + settings.callsign = callsign; + } + } + ); + + this.__defineGetter__( + "ssid", + function() { + return settings.ssid; + } + ); + + this.__defineSetter__( + "ssid", + function(ssid) { + if(typeof ssid == "undefined" || isNaN(ssid)) + throw "Invalid SSID."; + var ssid = Math.abs(ssid); + if(ssid > 15) { + throw format( + "Invalid SSID: %s", + ssid + ); + } + settings.ssid = ssid; + } + ); + + this.__defineGetter__( + "port", + function() { + return settings.port; + } + ); + + this.__defineSetter__( + "port", + function(port) { + if(typeof port == "undefined" || isNaN(port) || port < 0 || port > 15) + throw "AX25.KISSTNC: Invalid TNC radio port assignment"; + settings.port = port; + } + ); + + this.__defineGetter__( + "id", + function() { + return md5_calc( + format( + "%s%s%s", + this.callsign, + this.ssid, + this.serialPort, + this.port + ), + true + ); + } + ); + + this.__defineGetter__( + "timeout", + function() { + return settings.timeout; + } + ); + + this.__defineSetter__( + "timeout", + function(timeout) { + if(typeof timeout == "undefined" || isNaN(timeout)) + throw "Invalid timeout argument."; + settings.timeout = timeout; + } + ); + + this.__defineGetter__( + "txDelay", + function() { + return settings.txDelay; + } + ); + + this.__defineSetter__( + "txDelay", + function(txDelay) { + if(typeof txDelay == "undefined" || isNaN(txDelay)) + throw "Invalid txDelay argument."; + settings.txDelay = Math.abs(txDelay); + sendCommand(KISS_TXDELAY, this.txDelay / 10); + } + ); + + this.__defineGetter__( + "persistence", + function() { + return settings.persistence / 255; + } + ); + + this.__defineSetter__( + "persistence", + function(persistence) { + if( + typeof persistence == "undefined" + || + isNaN(persistence) + || + persistence > 1 + ) { + throw "Invalid persistence argument."; + } + settings.persistence = (persistence * 256) - 1; + sendCommand(KISS_PERSISTENCE, settings.persistence); + } + ); + + this.__defineGetter__( + "slotTime", + function() { + return settings.slotTime; + } + ); + + this.__defineSetter__( + "slotTime", + function(slotTime) { + if(typeof slotTime == "undefined" || isNaN(slotTime)) + throw "Invalid slotTime argument."; + settings.slotTime = Math.abs(slotTime); + sendCommand(KISS_SLOTTIME, this.slotTime / 10); + } + ); + + this.__defineGetter__( + "txTail", + function() { + return settings.txTail; + } + ); + + this.__defineSetter__( + "txTail", + function(txTail) { + if(typeof txTail == "undefined" || isNaN(txTail)) + throw "Invalid txTail argument."; + settings.txTail = Math.abs(txTail); + sendCommand(KISS_TXTAIL, this.txTail / 10); + } + ); + + this.__defineGetter__( + "fullDuplex", + function() { + return (settings.fullDuplex > 0) ? true : false; + } + ); + + this.__defineSetter__( + "fullDuplex", + function(fullDuplex) { + if(typeof fullDuplex != "boolean") + throw "Invalid fullDuplex argument."; + settings.fullDuplex = fullDuplex; + sendCommand(KISS_FULLDUPLEX, (fullDuplex) ? 1 : 0); + } + ); + + this.__defineGetter__( + "dataWaiting", + function() { + return (buffers.receive.length > 0) ? true : false; + } + ); + + this.__defineGetter__( + "dataPending", + function() { + return (buffers.send.length > 0) ? true : false; + } + ); + + this.setHardware = function(setting) { + if(typeof setting == "undefined" || isNaN(setting) || setting > 255) + throw "Invalid hardware setting."; + sendCommand(KISS_SETHARDWARE, setting); + } + + this.exitKISS = function() { + com.writeBin(KISS_FEND<<8|KISS_RETURN, 2); + com.writeBin(KISS_FEND, 1); + } - // Read a KISS frame from a TNC, return an AX.25 packet (array of bytes) less the flags and FCS - this.getKISSFrame = function() { + var getPacket = function() { var escaped = false; - var kissByte = this.handle.readBin(2); - if(kissByte != (KISS_FEND<<8)) + var kissByte = com.readBin(2); + if(kissByte != KISS_FEND<<8) return false; var kissFrame = new Array(); - // To do: add a timeout to this loop + var startTime = time(); while(kissByte != KISS_FEND) { - kissByte = this.handle.readBin(1); + if(time() - startTime > settings.timeout) { + throw format( + "Timeout reading packet from KISS interface %s, %s-%s", + settings.name, + settings.callsign, + settings.ssid + ); + } + kissByte = com.readBin(1); if(kissByte == -1) continue; - if((kissByte & KISS_FESC) == KISS_FESC) { + if((kissByte&KISS_FESC) == KISS_FESC) { escaped = true; continue; } - if(escaped && (kissByte & KISS_TFESC) == KISS_TFESC) { + if(escaped && kissByte&KISS_TFESC == KISS_TFESC) kissFrame.push(KISS_FESC); - } else if(escaped && (kissByte & KISS_TFEND) == KISS_TFEND) { + else if(escaped && kissByte&KISS_TFEND == KISS_TFEND) kissFrame.push(KISS_FEND); - } else if(kissByte != KISS_FEND) { + else if(kissByte != KISS_FEND) kissFrame.push(kissByte); - } escaped = false; } - return kissFrame; - } + + var packet = new AX25.Packet(kissFrame); + buffers.receive.push(packet); - // 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) { - if(p == undefined) return false; + if(AX25.logging) + packet.log(); + + return true; + } + + var sendPacket = function() { + if(buffers.send.length == 0) + return false; + var packet = buffers.send.shift(); + var kissFrame = packet.assemble(); 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); - } + com.writeBin((KISS_FEND<<8)|(settings.port<<4), 2); + for(var i = 0; i < kissFrame.length; i++) { + kissByte = kissFrame[i]; + if(kissByte == KISS_FEND) + com.writeBin(KISS_FESC<<8|KISS_TFEND, 2); + else if(kissByte == KISS_FESC) + com.writeBin(KISS_FESC<<8|KISS_TFESC, 2); + else + com.writeBin(kissByte, 1); } - this.handle.writeBin(KISS_FEND, 1); + com.writeBin(KISS_FEND, 1); + if(AX25.logging) + packet.log(); + return true; } - this.beacon = function() { - var a = new ax25packet(); - a.assemble("BEACON", 0, this.callsign, this.ssid, false, U_FRAME_UI, PID_NONE, stringToByteArray(system.name)); - this.sendKISSFrame(a.raw); + this.receive = function() { + return (this.dataWaiting) ? buffers.receive.shift() : undefined; } -} -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 )); + this.send = function(packet) { + if(typeof packet == "undefined" || !(packet instanceof AX25.Packet)) + throw "AX25.KISSTNC: Unable to send invalid packets"; + buffers.send.push(packet); + } + + this.cycle = function() { + while(getPacket()) { + } + while(sendPacket()) { + } + } + + this.serialPort = serialPort; + this.baudRate = baudRate; + this.callsign = callsign; + this.ssid = ssid; + + AX25.tncs[this.id] = this; } -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)); +AX25.Packet = function(frame) { + + var properties = { + 'destinationCallsign' : "", + 'destinationSSID' : 0, + 'sourceCallsign' : "", + 'sourceSSID' : 0, + 'repeaterPath' : [ ], + 'pollFinal' : false, + 'command' : 0, + 'response' : 0, + 'type' : 0, + 'nr' : 0, + 'ns' : 0, + 'pid' : PID_NONE, + 'info' : [ ] + }; + + this.__defineGetter__( + "destinationCallsign", + function() { + if(!testCallsign(properties.destinationCallsign)) + throw "AX25.Packet: Invalid destination callsign in received packet."; + return properties.destinationCallsign; + } + ); + + this.__defineSetter__( + "destinationCallsign", + function(callsign) { + if(typeof callsign == "undefined" || !testCallsign(callsign)) + throw "AX25.Packet: Invalid destination callsign assignment."; + properties.destinationCallsign = callsign; + } + ); + + this.__defineGetter__( + "destinationSSID", + function() { + if(properties.destinationSSID < 0 || properties.destinationSSID > 15) + throw "AX25.Packet: Invalid SSID in received packet."; + return properties.destinationSSID; + } + ); + + this.__defineSetter__( + "destinationSSID", + function(ssid) { + if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15) + throw "AX25.Packet: Invalid destination SSID assignment."; + properties.destinationSSID = ssid; + } + ); + + this.__defineGetter__( + "sourceCallsign", + function() { + if(!testCallsign(properties.sourceCallsign)) + throw "AX25.Packet: Invalid source callsign in received packet."; + return properties.sourceCallsign; + } + ); + + this.__defineSetter__( + "sourceCallsign", + function(callsign) { + if(typeof callsign == "undefined" || !testCallsign(callsign)) + throw "AX25.Packet: Invalid source callsign assignment."; + properties.sourceCallsign = callsign; + } + ); + + this.__defineGetter__( + "sourceSSID", + function() { + if(properties.sourceSSID < 0 || properties.sourceSSID > 15) + throw "AX25.Packet: Invalid SSID in received packet."; + return properties.sourceSSID; + } + ); + + this.__defineSetter__( + "sourceSSID", + function(ssid) { + if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15) + throw "AX25.Packet: Invalid source SSID assignment."; + properties.sourceSSID = ssid; + } + ); + + this.__defineGetter__( + "repeaterPath", + function() { + return properties.repeaterPath; + } + ); + + this.__defineSetter__( + "repeaterPath", + function(repeaters) { + if(typeof repeaters == "undefined" || !Array.isArray(repeaters)) + throw "AX25.Packet: Repeater path must be an array of { callsign, ssid } objects."; + for(var r = 0; r < repeaters.length; r++) { + if( + !repeaters[r].hasOwnProperty('callsign') + || + !testCallsign(repeaters[r].callsign) + ) { + throw "AX25.Packet: Repeater path must be an array of valid { callsign, ssid } objects."; + } + if( + !repeaters[r].hasOwnProperty('ssid') + || + repeaters[r].ssid < 0 + || + repeaters[r].ssid > 15 + ) { + throw "AX25.Packet: Repeater path must be an array of valid { callsign, ssid } objects."; } - var repeaterSSID = (parseInt(repeater[1])<<1); - if(i == this.repeaters.length - 1) - repeaterSSID |= (1<<0); - this.raw.push(repeaterSSID); } + properties.repeaterPath = repeaters; } - 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); + ); + + this.__defineGetter__( + "pollFinal", + function() { + return (properties.pollFinal == 1) ? true : false; } - if((this.control & I_FRAME) == I_FRAME || (this.control & S_FRAME) == S_FRAME) - this.nr = ((this.control & NR)>>>5); - } + ); + + this.__defineSetter__( + "pollFinal", + function(pollFinal) { + if(typeof pollFinal != "boolean") + throw "AX25.Packet: Invalid poll/final bit assignment (should be boolean.)"; + properties.pollFinal = (pollFinal) ? 1 : 0; + } + ); - 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.__defineGetter__( + "command", + function() { + return (properties.command == 1) ? true : false; + } + ); + + this.__defineSetter__( + "command", + function(command) { + if(typeof command != "boolean") + throw "AX25.Packet: Invalid command bit assignment (should be boolean.)"; + properties.command = (command) ? 1 : 0; + properties.response = (command) ? 0 : 1; + } + ); + + this.__defineGetter__( + "response", + function() { + return (properties.response == 1) ? true : false; + } + ); + + this.__defineSetter__( + "response", + function(response) { + if(typeof response != "boolean") + throw "AX25.Packet: Invalid response bit assignment (should be boolean.)"; + properties.response = (response) ? 1 : 0; + properties.command = (response) ? 0 : 1; + } + ); + + /* Assemble and return a control octet based on the properties of this + packet. (Note that there is no corresponding setter - the control + field is always generated based on packet type, poll/final, and the + N(S) and N(R) values if applicable, and must always be fetched from + this getter. */ + this.__defineGetter__( + "control", + function() { + var control = properties.type; + if(properties.type == I_FRAME || (properties.type&U_FRAME) == S_FRAME) + control|=(properties.nr<<5); + if(properties.type == I_FRAME) + control|=(properties.ns<<1); + if(this.pollFinal) + control|=(properties.pollFinal<<4); + return control; + } + ); + + this.__defineGetter__( + "type", + function() { + return properties.type; + } + ); + + this.__defineSetter__( + "type", + function(type) { + if(typeof type == undefined || isNaN(type)) + throw "AX25.Packet: Invalid frame type assignment."; + properties.type = type; + } + ); + + this.__defineGetter__( + "nr", + function() { + return properties.nr; + } + ); + + this.__defineSetter__( + "nr", + function(nr) { + if(typeof nr == "undefined" || isNaN(nr) || nr < 0 || nr > 7) + throw "AX25.Packet: Invalid N(R) assignment."; + properties.nr = nr; + } + ); + + this.__defineGetter__( + "ns", + function() { + return properties.ns; + } + ); + + this.__defineSetter__( + "ns", + function(ns) { + if(typeof ns == "undefined" || isNaN(ns) || ns < 0 || ns > 7) + throw "AX25.Packet: Invalid N(S) assignment."; + properties.ns = ns; + } + ); + + this.__defineGetter__( + "pid", + function() { + if(properties.pid == 0) + return undefined; + else + return properties.pid; + } + ); + + this.__defineSetter__( + "pid", + function(pid) { + if(typeof pid == undefined || isNaN(pid)) + throw "AX25.Packet: Invalid PID field assignment."; + if(properties.type == I_FRAME || properties.type == U_FRAME_UI) + properties.pid = pid; + else + throw "AX25.Packet: PID can only be set on I and UI frames (set the frame type first, accordingly.)"; + } + ); + + this.__defineGetter__( + "info", + function() { + if(properties.info.length < 1) + return undefined; + else + return properties.info; + } + ); + + this.__defineSetter__( + "info", + function(info) { + if(typeof info == "undefined") + throw "AX25.Packet: Invalid information field assignment."; + if(properties.type == I_FRAME || properties.type == U_FRAME) + properties.info = info; + else + throw "AX25.Packet: Info field can only be set on I and UI frames (set the frame type first, accordingly.)"; + } + ); + + /* You can compare this value against the 'ID' properties of your + AX25.Client objects to route an inbound packet to the appropriate + client (or create a new one.) + + Example: + + if(AX25.clients.hasOwnProperty(packet.clientID)) + AX25.clients[packet.clientID].receivePacket(packet); + else + var a = new AX25.client(tnc, packet); */ + this.__defineGetter__( + 'clientID', + function() { + return md5_calc( + format( + "%s%s%s%s", + properties.sourceCallsign, + properties.sourceSSID, + properties.destinationCallsign, + properties.destinationSSID + ), + true + ); + } + ); + + this.disassemble = function(frame) { + + // Address field + + // Address - destination subfield + var field = frame.splice(0, 6); + for(var f = 0; f < field.length; f++) + properties.destinationCallsign += ascii(field[f]>>1); + field = frame.shift(); + properties.destinationSSID = (field&A_SSID)>>1; + properties.command = (field&A_CRH)>>7; + + // Address - source subfield + field = frame.splice(0, 6); + for(var f = 0; f < field.length; f++) + properties.sourceCallsign += ascii(field[f]>>1); + field = frame.shift(); + properties.sourceSSID = (field&A_SSID)>>1; + properties.response = (field&A_CRH)>>7; + + // Address - repeater path + while(field&1 == 0) { + field = frame.splice(0, 6); + var repeater = { + 'callsign' : "", + 'ssid' : 0 + }; + for(var f = 0; f < field.length; f++) + repeater.callsign += ascii(field[f]>>1); + field = frame.shift(); + repeater.ssid = (field&A_SSID)>>1; + properties.repeaterPath.push(repeater); + } + + // Control field + var control = frame.shift(); + properties.pollFinal = (control&PF)>>4; + if((control&U_FRAME) == U_FRAME) { + properties.type = control&U_FRAME_MASK; + if(properties.type == U_FRAME_UI) { + properties.pid = frame.shift(); + properties.info = frame; } + } else if((control&U_FRAME) == S_FRAME) { + properties.type = control&S_FRAME_MASK; + properties.nr = (control&NR)>>5; + } else if((control&1) == I_FRAME) { + properties.type = I_FRAME; + properties.nr = (control&NR)>>5; + properties.ns = (control&NS)>>1; + properties.pid = frame.shift(); + properties.info = frame; + } else { + // This shouldn't be possible + throw "Invalid packet."; + } + + }; + + this.assemble = function() { + + // Try to catch a few obvious derps + if(properties.destinationCallsign.length == 0) + throw "AX25.Packet: Destination callsign not set."; + if(properties.sourceCallsign.length == 0) + throw "AX25.Packet: Source callsign not set."; + if( + properties.type == I_FRAME + && + ( + !properties.hasOwnProperty('pid') + || + !properties.hasOwnProperty('info') + || properties.info.length == 0 + ) + ) { + throw "I or UI frame with no payload."; + } + + var frame = []; + + // Address field + + // Address - destination subfield - encode callsign and SSID + for(var c = 0; c < 6; c++) { + frame.push( + ( + (properties.destinationCallsign.length - 1 >= c) + ? + ascii(properties.destinationCallsign[c]) + : + 32 + )<<1 + ); + } + frame.push((properties.command<<7)|(properties.destinationSSID<<1)); + + // Address - source subfield - encode callsign and SSID + for(var c = 0; c < 6; c++) { + frame.push( + ( + (properties.sourceCallsign.length - 1 >= c) + ? + ascii(properties.sourceCallsign[c]) + : + 32 + )<<1 + ); } - 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]); + frame.push( + (properties.response<<7) + | + (properties.sourceSSID<<1) + | + ((properties.repeaterPath.length < 1) ? 1 : 0) + ); + + // Address - tack on a repeater path if one was specified + for(var r = 0; r < properties.repeaterPath.length; r++) { + for(var c = 0; c < 6; c++) { + frame.push( + ( + (properties.repeaterPath[r].callsign.length - 1 >= c) + ? + ascii(properties.repeaterPath[r].callsign[c]) + : + 32 + )<<1 + ); } + frame.push( + (properties.repeaterPath[r].ssid<<1) + | + ((r == properties.repeaterPath.length - 1) ? 1 : 0) + ); + } + + // Control field + frame.push(this.control); + + // PID field (I and UI frames only) + if( + properties.pid + && + (properties.type == I_FRAME || properties.type == U_FRAME_UI) + ) { + frame.push(properties.pid); } - // 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); + + // Info field (I and UI frames only) + if( + properties.info.length > 0 + && + (properties.type == I_FRAME || properties.type == U_FRAME_UI) + ) { + for(var i = 0; i < properties.info.length; i++) + frame.push(properties.info[i]); + } + + return frame; } - 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); + this.log = function() { + var out = format( + "%s-%s -> %s-%s ", + this.sourceCallsign, + this.sourceSSID, + this.destinationCallsign, + this.destinationSSID + ); + if(this.repeaterPath.length > 0) { + out += "via "; + for(var r = 0; r < this.repeaterPath.length; r++) { + out += format( + "%s-%s, ", + this.repeaterPath[r].callsign, + this.repeaterPath[r].ssid + ); + } + } + switch(this.type) { + case U_FRAME_UI: + out += "UI"; + break; + case U_FRAME_SABM: + out += "SABM"; + break; + case U_FRAME_DISC: + out += "DISC"; + break; + case U_FRAME_DM: + out += "DM"; + break; + case U_FRAME_UA: + out += "UA"; + break; + case U_FRAME_FRMR: + out += "FRMR"; + break; + case S_FRAME_RR: + out += "RR"; + break; + case S_FRAME_RNR: + out += "RNR"; + break; + case S_FRAME_REJ: + out += "REJ"; + break; + case I_FRAME: + out += "I"; + break; + default: + // Unlikely + out += "Unknown frame type"; + break; + } + + out += format( + ", C: %s, R: %s, ", + (this.command) ? 1 : 0, + (this.response) ? 1 : 0 + ); + + out += format("PF: %s", (this.pollFinal) ? 1 : 0); + + if((this.control&U_FRAME) == S_FRAME || (this.control&1) == I_FRAME) + out += format(", NR: %s", this.nr); + + if((this.control&1) == I_FRAME) + out += format(", NS: %s,", this.ns); + + if( + (this.control&1) == I_FRAME + || + (this.control&U_FRAME_MASK) == U_FRAME_UI + ) { + out += format(" PID: %s, Info: ", this.pid); + for(var i = 0; i < this.info.length; i++) + out += ascii(this.info[i]); + } + + log(7, out); } + + if(typeof frame != "undefined") + this.disassemble(frame); + } -// 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])); +AX25.Client = function(tnc, packet) { + + if(!(tnc instanceof AX25.KISSTNC)) + throw "AX25.Client: invalid 'tnc' argument"; + + var settings = { + 'maximumRetries' : 5, // Maximum failed T1 or T3 polls (per timer) + 'maximumErrors' : 5, // Maximum (FRMR) errors before disconnect + 'timer3Interval' : 60 // Period of inactivity before T3 poll + }; + + var properties = { + 'tnc' : tnc, // Reference to an AX25.KISSTNC obj. + 'callsign' : "", // Callsign of the remote side + 'ssid' : 0, // SSID of the remote side + 'sendState' : 0, // N(S) of next I frame we'll send + 'receiveState' : 0, // N(S) of next expected I frame + 'remoteReceiveState' : 0, // Remote side's last reported N(R) + 'timer1' : 0, // Waiting acknowledgement if not 0 + 'timer1Retries' : 0, // Count of unacknowledged T1 polls + 'timer3Retries' : 0, // Count of failed keepalive polls + 'errors' : 0, // Persistent across link resets + 'repeaterPath' : [], // Same format as in AX25.Packet + 'timer3' : time(), // Updated on receipt of any packet + 'connected' : false, // True if link established + 'connecting' : false, // True if we sent SABM + 'disconnecting' : false, // True if we sent DISC + 'remoteBusy' : false, // True if remote sent RNR + 'rejecting' : false, // True if we sent FRMR + 'sentReject' : false // True if we sent REJ + }; + + var buffers = { + 'send' : [], // Data to be sent in I frames + 'receive' : [], // Data received in I frames + 'outgoing' : [], // Actual packets to be sent + 'incoming' : [], // Actual received packets + 'sent' : [] // I frames sent but not yet acknowledged + }; + + /* For internal use only. Scripts can use AX25.Client.send() without + having to consult this property; data will only actually be sent when + AX25.Client.cycle() is called, and when flow control permits. */ + this.__defineGetter__( + "canSend", + function() { + return ( + properties.connected + && + buffers.send.length > 0 + && + !properties.remoteBusy + && + distanceBetween( + properties.sendState, + properties.remoteReceiveState, + 8 + ) < 7 + ) ? true : false; + } + ); + + this.__defineGetter__( + "dataWaiting", + function() { + return (buffers.receive.length > 0) ? true : false; + } + ); + + this.__defineGetter__( + "connected", + function() { + return properties.connected; + } + ); + + /* When waiting for acknowledgement of (certain) sent packets, wait this + many seconds before polling the remote side again. + + You may wonder how I arrived at this formula (particularly the '* 20'.) + The AX.25 specification is deliberately vague about how long this should + be. Their suggested duration is far too short, resulting in our side + pinging the remote station every couple of seconds, not leaving it + enough time to prepare and send a response, and causing us to reach our + retry limit in short order. I find that ten times the round-trip time + of the largest possible frame seems to work without being excessively + long. ((Bits / baud rate) * hops to client) * 20. */ + this.__defineGetter__( + "timer1Timeout", + function() { + return ( + ( + (AX25.maximumPacketSize / properties.tnc.baudRate) + * + (properties.repeaterPath.length + 1) + ) + * 20 + ); + } + ); + + this.__defineGetter__( + "callsign", + function() { + return properties.callsign; + } + ); + + this.__defineSetter__( + "callsign", + function(callsign) { + if(typeof callsign == "undefined" || !testCallsign(callsign)) + throw "AX25.Client: Invalid destination callsign assignment"; + properties.callsign = callsign; + } + ); + + this.__defineGetter__( + "ssid", + function() { + return properties.ssid; + } + ); + + this.__defineSetter__( + "ssid", + function(ssid) { + if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15) + throw "AX25.Client: Invalid destination SSID assignment"; + properties.ssid = ssid; + } + ); + + /* You can compare the 'clientID' property of an *inbound* packet against + this value to match it up with an existing AX25.Client object. + + Example: + + if(AX25.clients.hasOwnProperty(packet.clientID)) + AX25.clients[packet.clientID].receivePacket(packet); + else + var a = new AX25.Client(tnc, packet); */ + this.__defineGetter__( + "id", + function() { + return md5_calc( + format( + "%s%s%s%s", + properties.callsign, + properties.ssid, + properties.tnc.callsign, + properties.tnc.ssid + ), + true + ); + } + ); + + // Resets properties that shouldn't persist after a link reset or disconnect + var resetVariables = function () { + properties.sendState = 0; + properties.receiveState = 0; + properties.remoteReceiveState = 0; + properties.timer1 = 0; + properties.timer1Retries = 0; + properties.timer3Retries = 0; + properties.timer3 = time(); + properties.remoteBusy = false; + properties.sentReject = false; + properties.connected = false; + properties.connecting = false; + properties.disconnecting = false; + } + + // Returns a new AX25.Packet with the Address field pre-populated + var makePacket = function() { + var packet = new AX25.Packet(); + packet.destinationCallsign = properties.callsign; + packet.destinationSSID = properties.ssid; + packet.sourceCallsign = properties.tnc.callsign; + packet.sourceSSID = properties.tnc.ssid; + if(properties.repeaterPath.length > 0) + packet.repeaterPath = properties.repeaterPath; + return packet; + } + + /* Cancels or resets timer T1 as necessary, discards sent I frames that + have been acknowledged by the remote side. */ + var receiveAcknowledgement = function(nr) { + if(nr == properties.remoteReceiveState) + return; + properties.timer1 = 0; + properties.timer1Retries = 0; + properties.remoteReceiveState = nr; + for(var i = 0; i < buffers.sent.length; i++) { + if(buffers.sent[i].ns != wrapAround((nr - 1), 8)) + continue; + buffers.sent.splice(0, i + 1); + } + if(buffers.sent.length > 0) + properties.timer1 = time(); } - 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]); + /* Sends 'packet' to the TNC. If 'packet is an I frame, stuffs it into + buffers.sent so that it can be resent later if necessary, or discarded + once it's acknowledged. */ + var sendPacket = function(packet) { + properties.tnc.send(packet); + if(packet.type == I_FRAME) { + buffers.sent.push(packet); + properties.timer1 = time(); + } } - 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; + /* Shifts the next payload off of buffers.send, creates an I frame around + it, pushes that I frame into buffers.outgoing. 'command' and 'pollFinal' + are boolean toggles for the C, R, and P/F bits of the I frame. */ + var sendIFrame = function(command, pollFinal) { + if(buffers.send.length == 0) + return false; + var info = buffers.send.shift(); + var packet = makePacket(); + packet.command = command; + packet.type = I_FRAME; + packet.nr = properties.receiveState; + packet.ns = properties.sendState; + packet.pollFinal = pollFinal; + packet.pid = PID_NONE; + packet.info = info; + properties.sendState = (properties.sendState + 1) % 8; + buffers.outgoing.push(packet); + return true; } - this.clientID = destination.replace(/\s/, "") + destinationSSID + source.replace(/\s/, "") + sourceSSID; - - this.init = function() { - 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.wait = false; - this.connected = false; - this.sentIFrames = []; - this.lastPacket = false; - this.expectUA = false; + + /* Resends I frames (called when the remote side sends a REJ frame.) + receiveAcknowledgement() should be called on the receipt of any S or I + frame and (on receipt of a REJ frame) prior to calling this function. + That way, buffers.sent[0] will assuredly be the oldest of our sent I + frames that we're still waiting for acknowledgement on. */ + var resendIFrames = function(nr) { + if(buffers.sent.length < 1 || buffers.sent[0].ns != nr) + return false; + for(var i = 0; i < buffers.sent.length; i++) { + buffers.sent[i].nr = properties.receiveState; + buffers.outgoing.push(buffers.sent[i]); + } + return true; } - this.init(); + this.connect = function(callsign, ssid) { + resetVariables(); + this.callsign = callsign; + this.ssid = (typeof ssid == "undefined") ? 0 : ssid; + var packet = makePacket(); + packet.type = U_FRAME_SABM; + packet.command = true; + packet.pollFinal = true; + buffers.outgoing.push(packet); + properties.connecting = true; + properties.timer1 = time(); + } + + this.disconnect = function() { + resetVariables(); + for(var b in buffers) + buffers[b] = []; + var packet = makePacket(); + packet.type = U_FRAME_DISC; + packet.command = true; + packet.pollFinal = true; + buffers.outgoing.push(packet); + properties.disconnecting = true; + properties.timer1 = time(); + } - /* 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.init(); - this.connected = true; - } - 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.expectUA) { - this.expectUA = false; - 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 { - a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM); - this.init(); - this.expectUA = true; - } - } 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.init(); - } 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) { - if(p.hasOwnProperty("information") && p.information.length > 0) - retval = p.information; - 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 = this.sentIFrames[p.nr - 1]; - this.nr = p.nr; - } else if((p.control & S_FRAME_RNR) == S_FRAME_RNR) { - this.nr = p.nr - 1; - } 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(this.ssv >= this.nr) - // We haven't exceeded the flow control window, so no need to wait before sending more I frames - this.wait = false; - if(p.nr == 7 && this.sentIFrames.length >= 7) { - // Client acknowledges the entire sequence, we can ditch our stored sent packets - this.sentIFrames = this.sentIFrames.slice(7); - return retval; - } else if(this.resend && p.nr < this.sentIFrames.length) { - a = 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(this.ssv >= this.nr) - this.wait = false; - 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++; - this.rsv = this.rsv % 8; - 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; + this.cycleTimers = function() { + + var packet = makePacket(); + packet.pollFinal = true; + packet.command = true; + + if( + properties.connected + && + ( + properties.timer1Retries > settings.maximumRetries + || + properties.timer3Retries > settings.maximumRetries + ) + ) { + packet.type = U_FRAME_DM; + resetVariables(); + } else if( + properties.timer1 > 0 + && + time() - properties.timer1 > this.timer1Timeout + ) { + if(properties.connecting) { + packet.type = U_FRAME_SABM; + } else if(properties.disconnecting) { + packet.type = U_FRAME_DISC; + } else if(properties.connected) { + packet.type = S_FRAME_RR; + packet.nr = properties.receiveState; } + properties.timer1Retries++; + properties.timer1 = time(); + } else if( + properties.connected + && + time() - properties.timer3 > settings.timer3Interval + ) { + packet.type = S_FRAME_RR; + packet.nr = properties.receiveState; + properties.timer3Retries++; + properties.timer3 = time(); } else { - return retval; + packet = false; } - this.sendPacket(a); - return retval; + + if(!packet || packet.type == I_FRAME) + return; + + buffers.outgoing.push(packet); } - - // Send ax25Packet object 'a' to an ax25Client - this.sendPacket = function(a) { - this.lastPacket = a; - this.kissTNC.sendKISSFrame(a.raw); - a.logPacket(); + + this.send = function(info) { + if(typeof info == "undefined") + throw "AX25.Client: Tried to send an empty packet"; + buffers.send.push(info); + } + + this.sendString = function(str) { + if(typeof str != "string") + throw "AX25.Client.sendString: Invalid string"; + buffers.send.push(stringToByteArray(str)); } - // 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++; - this.ssv = this.ssv % 8; - if(this.ssv < this.nr) - /* If we send again, we will exceed the flow control window. We - should wait for the client to catch up before sending more. */ - this.wait = true; - this.sentIFrames.push(a); + this.receive = function() { + if(!this.dataWaiting) + return undefined; + return buffers.receive.shift(); } + + this.receiveString = function() { + if(!this.dataWaiting) + return undefined; + return byteArrayToString(buffers.receive.shift()); + } + + this.receivePacket = function(packet) { + if(typeof packet == "undefined" || !(packet instanceof AX25.Packet)) + throw "AX25.Client: Tried to handle an invalid packet"; - // 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(!this.connected && i < 5){ - this.sendPacket(a); - this.expectUA = true; - mswait(3000); - this.receive(); - i++; - } - if(this.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); + if(properties.callsign == "") { + properties.callsign = packet.sourceCallsign; + properties.ssid = packet.sourceSSID; + AX25.clients[this.id] = this; } + + buffers.incoming.push(packet); + properties.timer3 = time(); + properties.timer3Retries = 0; } + + this.handlePacket = function(packet) { + + // Update the repeater path, unsetting the H bit as we go + properties.repeaterPath = []; + for(var r = packet.repeaterPath.length - 1; r >= 0; r--) { + // Drop any packet that was meant for a repeater and not us + if(packet.repeaterPath[r].ssid&A_CRH == 0) + return false; + packet.repeaterPath[r].ssid|=(0<<7); + properties.repeaterPath.push(packet.repeaterPath[r]); + } - // 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(this.connected && i < 5){ - this.sendPacket(a); - this.expectUA = true; - mswait(3000); - this.receive(); - i++; - } - if(this.connected) { - this.connected = false; - log(LOG_INFO, this.callsign + "-" + this.ssid + " failed to acknowledge U_FRAME_DISC from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid); + var response = makePacket(); + response.pollFinal = packet.pollFinal; + response.response = packet.command; + + if(!properties.connected) { + + switch(packet.type) { + + case U_FRAME_SABM: + resetVariables(); + properties.connected = true; + response.type = U_FRAME_UA; + break; + + case U_FRAME_UA: + if(properties.connecting) { + resetVariables(); + properties.connected = true; + response = false; + break; + } // Else, fall through to default + + case U_FRAME_UI: + buffers.receive.push(packet.info); + if(!packet.pollFinal) { + response = false; + break; + } // Else, fall through to default + + default: + response.type = U_FRAME_DM; + response.pollFinal = true; + break; + + } + } else { - log(LOG_INFO, this.callsign + "-" + this.ssid + " disconnected from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid); + + switch(packet.type) { + + case U_FRAME_SABM: + resetVariables(); + properties.connected = true; + response.type = U_FRAME_UA; + break; + + case U_FRAME_DISC: + resetVariables(); + response.type = U_FRAME_UA; + break; + + case U_FRAME_UA: + if(properties.connecting || properties.disconnecting) { + resetVariables(); + properties.connected = (properties.connecting) ? true : false; + response = false; + break; + } // Else, fall through to default + + case U_FRAME_UI: + buffers.receive.push(packet.info); + if(packet.pollFinal) { + response.type = S_FRAME_RR; + response.nr = properties.receiveState; + } else { + response = false; + } + break; + + case U_FRAME_DM: + resetVariables(); + response = false; + break; + + case U_FRAME_FRMR: + properties.errors++; + if(errors >= settings.maximumErrors) { + response.type = U_FRAME_DISC; + properties.disconnecting = true; + } else { + response.type = U_FRAME_SABM; + properties.connecting = true; + } + properties.timer1 = time(); + break; + + case S_FRAME_RR: + properties.remoteBusy = false; + receiveAcknowledgement(packet.nr); + if(packet.pollFinal && packet.command) { + response.type = S_FRAME_RR; + response.nr = properties.receiveState; + } else { + response = false; + } + break; + + case S_FRAME_RNR: + properties.remoteBusy = true; + receiveAcknowledgement(packet.nr); + if(packet.pollFinal) { + response.type = S_FRAME_RR; + response.nr = properties.receiveState; + } else { + response = false; + } + break; + + case S_FRAME_REJ: + properties.remoteBusy = false; + receiveAcknowledgement(packet.nr); + if(packet.pollFinal) { + response.type = S_FRAME_RR; + response.nr = properties.receiveState; + buffers.outgoing.push(response); + } + if(!resendIFrames(packet.nr)) { + response.type = U_FRAME_SABM; + properties.connecting = true; + } else { + response = false; + } + break; + + case I_FRAME: + receiveAcknowledgement(packet.nr); + if(packet.ns != properties.receiveState) { + if(packet.pollFinal || !properties.sentReject) { + response.type = S_FRAME_REJ; + response.command = true; + response.nr = properties.receiveState; + properties.sentReject = true; + } else { + response = false; + } + } else { + properties.sentReject = false; + properties.receiveState = (properties.receiveState + 1) % 8; + buffers.receive.push(packet.info); + if(this.canSend && !packet.pollFinal) { + sendIFrame(false, false); + response = false; + } else { + response.type = S_FRAME_RR; + response.nr = properties.receiveState; + } + } + break; + + default: + response = false; + break; + + } + + } + + if(response) + buffers.outgoing.push(response); + + } + + this.cycle = function() { + + // Process any incoming packets and queue up responses + while(buffers.incoming.length > 0) { + this.handlePacket(buffers.incoming.shift()); + } + + this.cycleTimers(); + + // Stuff any outgoing I frames into the buffer + while(this.canSend) { + if(!sendIFrame(true, false)) + break; } + + // Send any outgoing packets to the TNC + while(buffers.outgoing.length > 0) { + sendPacket(buffers.outgoing.shift()); + } + } -} + + /* If this object was instantiated with a 'packet' argument, we assume that + it's a packet received from the remote side and stuff it into the + incoming buffer. When .cycle() is first called, that packet will be + processed and responded to. */ + if(typeof packet != "undefined") + this.receivePacket(packet); + +} \ No newline at end of file