diff --git a/CHANGELOG.md b/CHANGELOG.md index af3b29dcda8451f38db4f42e543114f23de416c5..a90c9a6e9ba164dc3e6d2000864fe2f7da9df14d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog -## 2.1.4 +## 3.0.0 +* Moved all elements of AX.25 out into its own library (https://git.qoto.org/digipex/ax25). ## 2.1.3 diff --git a/lib/kiss/app_info.rb b/lib/kiss/app_info.rb index 0c0ab7dd0c3a5d601d9f529ad5de10d331b6237b..27a5a22ede6bef7be1273b9703106527908639de 100644 --- a/lib/kiss/app_info.rb +++ b/lib/kiss/app_info.rb @@ -1,3 +1,3 @@ module Kiss - VERSION = "2.1.4" + VERSION = "3.0.0" end diff --git a/lib/kiss/kiss.rb b/lib/kiss/kiss.rb index c2850eb009258928a3d52e3be0418a467364861e..42d36070a044cc24d3d6cc013bd3d8b87fafcd60 100644 --- a/lib/kiss/kiss.rb +++ b/lib/kiss/kiss.rb @@ -4,6 +4,101 @@ module Kiss module Kiss include Abstractify::Abstract - abstract :write_interface, :read_interface, :connect, :close + abstract :write_interface, :read_interface, :connect, :close, :read_datagram, :write_generic_command, :write_datagram, :write_tx_delay, :write_persistence, :write_slot_time, :write_tx_tail, :write_full_duplex, :write_set_hardware, :write_exit_kiss_mode + + ## + # Reads raw data as a byte array off the underlying stream. This data is + # before escapaing, or extracting from the frame. + protected + def read_interface(*args, **kwargs) + end + + ## + # Writes raw bytes to the underlying stream. The bytes should already begin + # escaped and framed by this point. + protected + def write_interface(data, *args, **kwargs) + end + + ## + # Prepares the underlying connection for reading and writing. What this + # means depends on the underlying implementation. It may do nothing if + # there are no initialization steps. Otherwise it will initiate the + # connection to the TNC and send the initial data needed to get it to + # enter KISS mode. + public + def connect(mode_init=nil, *args, **kwargs) + end + + ## + # Closes the underlying connection and frees up any relevant resources. + # After the connection is closed read and write opperations will usually + # fail. + public + def close(*args, **kwargs) + end + + ## + # Read and return the next datagram waiting in the buffer. Nil if there + # is no datagram waiting. + public + def read_datagram(*args, **kwargs) + end + + ## + # This method sends any command to the underlying KISS device. All + # outgoing calls, methods with write_*, ultimately calls this method + # as the underlying call to KISS. + public + def write_generic_command(command, port, value, *args, **kwargs) + end + + ## + # This command nibble should be set anytime a datagram is being sent for transmission. + public + def write_datagram(packet_data, port = 0, *args, **kwargs) + end + + ## + # The amount of time to wait between keying the transmitter and beginning to send data (in 10 ms units). + public + def write_tx_delay(tx_delay, port = 0, *args, **kwargs) + end + + ## + # The persistence parameter. Persistence=Data*256-1. Used for CSMA. + public + def write_persistence(persistence, port = 0, *args, **kwargs) + end + + ## + # Slot time in 10 ms units. Used for CSMA. + public + def write_slot_time(slot_time, port = 0, *args, **kwargs) + end + + ## + # The length of time to keep the transmitter keyed after sending the data (in 10 ms units). + public + def write_tx_tail(tx_tail, port = 0, *args, **kwargs) + end + + ## + # True to turn on full_duplex, false to turn it off. + public + def write_full_duplex(full_duplex, port = 0, *args, **kwargs) + end + + ## + # The meaning of this command is device dependent. + public + def write_set_hardware(value, port = 0, *args, **kwargs) + end + + ## + # The meaning of this command is device dependent. + public + def write_exit_kiss_mode(*args, **kwargs) + end end end diff --git a/lib/kiss/kiss_abstract.rb b/lib/kiss/kiss_abstract.rb index 9ac88cc125d5be85462ba399cb2bdea9e018fddd..2c19610378fc0c3b4f4edc17297cea65f4423f91 100644 --- a/lib/kiss/kiss_abstract.rb +++ b/lib/kiss/kiss_abstract.rb @@ -57,138 +57,10 @@ module Kiss end private - def self.extract_path(start, raw_frame) - full_path = [] - - (2...start).each do |i| - path = identity_as_string(extract_callsign(raw_frame[i * 7..-1])) - if path and path.length > 0 - if raw_frame[i * 7 + 6] & 0x80 != 0 - full_path << [path, '*'].join - else - full_path << path - end - end - end - return full_path - end - - private - def self.parse_identity_string(identity_string) - # If we are parsing a spent token then first lets get rid of the astresick suffix. - if identity_string[-1] == '*' - identity_string = identity_string[0..-1] - end - - if identity_string.include? '-' - call_sign, ssid = identity_string.split('-') - else - call_sign = identity_string - ssid = 0 - end - - return {:callsign => call_sign, :ssid => ssid.to_i} - end - - private - def self.identity_as_string(identity) - if identity[:ssid] and identity[:ssid] > 0 - return [identity[:callsign], identity[:ssid].to_s].join('-') - else - return identity[:callsign] - end - end - - private - def self.extract_callsign(raw_frame) - callsign_as_array = raw_frame[0...6].map { |x| (x >> 1).chr } - callsign = callsign_as_array.join.strip - ssid = (raw_frame[6] >> 1) & 0x0f - ssid = (ssid == nil or ssid == 0 ? nil : ssid) - return {:callsign => callsign, :ssid => ssid} - end - - private - def self.encode_callsign(callsign) - call_sign = callsign[:callsign] - - enc_ssid = (callsign[:ssid] << 1) | 0x60 - - if call_sign.include? '*' - call_sign.gsub!(/\*/, '') - enc_ssid |= 0x80 - end - - while call_sign.length < 6 - call_sign = [call_sign, ' '].join - end - - return call_sign.chars.map { |p| p.ord << 1 } + [enc_ssid] - end - - private - def self.encode_frame(frame) - enc_frame = encode_callsign(parse_identity_string(frame[:destination].to_s)) + encode_callsign(parse_identity_string(frame[:source].to_s)) - - frame[:path].each do |hop| - enc_frame += encode_callsign(parse_identity_string(hop.to_s)) - end - - return enc_frame[0...-1] + [enc_frame[-1] | 0x01] + [SLOT_TIME] + [0xf0] + frame[:payload].chars.map { |c| c.ord } - end - - private - def self.decode_frame(raw_frame) - frame_len = raw_frame.length - - if frame_len > 16 - (0...frame_len - 2).each do |raw_slice| - # Is address field length correct? - if raw_frame[raw_slice] & 0x01 != 0 and ((raw_slice + 1) % 7) == 0 - i = (raw_slice.to_f + 1.0) / 7.0 - # Less than 2 callsigns? - if 1.0 < i and i < 11.0 - if raw_frame[raw_slice + 1] & 0x03 == 0x03 and [0xf0, 0xcf].include? raw_frame[raw_slice + 2] - payload_as_array = raw_frame[raw_slice + 3..-1].map { |b| b.chr } - payload = payload_as_array.join - destination = identity_as_string(extract_callsign(raw_frame)) - source = identity_as_string(extract_callsign(raw_frame[7..-1])) - path = extract_path(i.to_i, raw_frame) - return {:source => source, :destination => destination, :path => path, :payload => payload} - end - end - end - end - end - return nil - end - - private - def self.valid_frame(raw_frame) - frame_len = raw_frame.length - - if frame_len > 16 - (0...frame_len - 2).each do |raw_slice| - # Is address field length correct? - if raw_frame[raw_slice] & 0x01 != 0 and ((raw_slice + 1) % 7) == 0 - i = (raw_slice.to_f + 1.0) / 7.0 - # Less than 2 callsigns? - if 1.0 < i and i < 11.0 - if raw_frame[raw_slice + 1] & 0x03 == 0x03 and [0xf0, 0xcf].include? raw_frame[raw_slice + 2] - return true - end - end - end - end - end - return false - end - - private - def fill_buffer + def fill_buffer(*args, **kwargs) new_frames = [] read_buffer = [] - read_data = read_interface + read_data = read_interface(*args, **kwargs) while read_data and read_data.length > 0 split_data = [[]] read_data.each do |read_byte| @@ -229,7 +101,7 @@ module Kiss end end # Get anymore data that is waiting - read_data = read_interface + read_data = read_interface(*args, **kwargs) end new_frames.each do |new_frame| @@ -245,7 +117,7 @@ module Kiss private def read_bytes(*args, **kwargs) if @frame_buffer.length == 0 - fill_buffer + fill_buffer(*args, **kwargs) end if @frame_buffer.length > 0 @@ -257,48 +129,99 @@ module Kiss end end - private - def write_bytes(frame_bytes, port=0, *args, **kwargs) - kiss_packet = [FEND] + [KissAbstract.command_byte_combine(port, DATA_FRAME)] + - KissAbstract.escape_special_codes(frame_bytes) + [FEND] + public + def write_generic_command(command, port, value, *args, **kwargs) + @lock.synchronize do + raise ArgumentError.new ("command can not be nil") if command.nil? + raise RangeError.new("command must be an integer between 0 (inclusive) and 15 (inclusive) instead we got #{command}") if command < 0 or command >= 16 or command&0x0f!=command + raise ArgumentError.new ("port can not be nil") if port.nil? + raise RangeError.new("port must be an integer between 0 (inclusive) and 15 (inclusive) instead we got #{port}") if port < 0 or port >= 16 or port&0x0f!=port - write_interface(kiss_packet) + if value.nil? + write_interface([FEND] + [KissAbstract.command_byte_combine(port, command)] +[FEND], *args, **kwargs) + else + write_interface([FEND] + [KissAbstract.command_byte_combine(port, command)] + KissAbstract.escape_special_codes(value) + [FEND], *args, **kwargs) + end + end end - protected - def write_setting(command, value) - write_interface([FEND] + [command] + KissAbstract.escape_special_codes(value) + [FEND]) + public + def write_datagram(packet_data, port = 0, *args, **kwargs) + raise TypeError.new("packet_data can not be nil") if packet_data.nil? + raise TypeError.new("packet_data must be an array-like object that implements each, length, and []") if (not packet_data.respond_to?(:each)) or (not packet_data.respond_to?(:[])) or (not packet_data.respond_to?(:length)) + raise RangeError.new("packet_data must have a length of 1 or more") if packet_data.length <= 0 + + write_generic_command(DATA_FRAME, port, packet_data, *args, **kwargs) end public - def connect(mode_init=nil, *args, **kwargs) + def write_tx_delay(tx_delay, port = 0, *args, **kwargs) + raise TypeError.new("tx_delay can not be nil") if tx_delay.nil? + raise RangeError.new("tx_delay must be an integer value greater than or equal to 0 and less than 256") if tx_delay < 0 and tx_delay&0xff==tx_delay + + write_generic_command(TX_DELAY, port, tx_delay, *args, **kwargs) end public - def close(*args, **kwargs) + def write_persistence(persistence, port = 0, *args, **kwargs) + raise TypeError.new("persistence can not be nil") if persistence.nil? + raise RangeError.new("persistence must be an integer value greater than or equal to 0 and less than 256") if persistence < 0 and persistence&0xff==persistence + + write_generic_command(PERSISTENCE, port, persistence, *args, **kwargs) end public - def read(*args, **kwargs) - @lock.synchronize do - frame = self.read_bytes(*args, **kwargs) - if frame and frame.length > 0 - return KissAbstract.decode_frame(frame) - else - return nil - end + def write_slot_time(slot_time, port = 0, *args, **kwargs) + raise TypeError.new("slot_time can not be nil") if slot_time.nil? + raise RangeError.new("slot_time must be an integer value greater than or equal to 0 and less than 256") if slot_time < 0 and slot_time&0xff==slot_time + + write_generic_command(SLOT_TIME, port, slot_time, *args, **kwargs) + end + + public + def write_tx_tail(tx_tail, port = 0, *args, **kwargs) + raise TypeError.new("tx_tail can not be nil") if tx_tail.nil? + raise RangeError.new("tx_tail must be an integer value greater than or equal to 0 and less than 256") if tx_tail < 0 and tx_tail&0xff==tx_tail + + write_generic_command(TX_TAIL, port, tx_tail, *args, **kwargs) + end + + public + def write_full_duplex(full_duplex, port = 0, *args, **kwargs) + raise TypeError.new("full_duplex can not be nil") if full_duplex.nil? + raise TypeError.new("full_duplex must be either truthy or falsy") if full_duplex != (!!full_duplex) + + if full_duplex + write_generic_command(FULL_DUPLEX, port, 1, *args, **kwargs) + else + write_generic_command(FULL_DUPLEX, port, 0, *args, **kwargs) end end public - def write(frame, port=0, *args, **kwargs) + def write_set_hardware(value, port = 0, *args, **kwargs) + raise TypeError.new("value can not be nil") if value.nil? + + write_generic_command(SET_HARDWARE, port, value, *args, **kwargs) + end + + public + def write_exit_kiss_mode(*args, **kwargs) + write_generic_command(RETURN, 0, nil*args, **kwargs) + end + + public + def connect(mode_init=nil, *args, **kwargs) + end + + public + def close(*args, **kwargs) + end + + public + def read_datagram(*args, **kwargs) @lock.synchronize do - encoded_frame = KissAbstract.encode_frame(frame) - if KissAbstract.valid_frame(encoded_frame) - self.write_bytes(encoded_frame, *args, **kwargs) - else - raise IOError.new("frame was able to be encoded but was determined not to be valid") - end + return self.read_bytes(*args, **kwargs) end end end diff --git a/lib/kiss/kiss_serial.rb b/lib/kiss/kiss_serial.rb index 60f64ea80be02613e6c2761b2e41b3e004492fdd..0d14f8313ea3c28222b1959d928d8c0707dc36e3 100644 --- a/lib/kiss/kiss_serial.rb +++ b/lib/kiss/kiss_serial.rb @@ -28,8 +28,8 @@ module Kiss end protected - def read_interface - read_data = @serial.read(@read_bytes) + def read_interface(*args, **kwargs) + read_data = @serial.read(@read_bytes, *args, **kwargs) if read_data return read_data.chars.map { |c| c.ord } else @@ -38,18 +38,18 @@ module Kiss end protected - def write_interface(data) + def write_interface(data, *args, **kwargs) unless data.is_a? String data = data.map { |b| b.chr }.join end - @serial.write(data) + @serial.write(data, *args, **kwargs) end public def connect(mode_init=nil, *args, **kwargs) super - @serial = SerialPort.new(@com_port, @baud, @byte_size, @stop_bits, @parity) + @serial = SerialPort.new(@com_port, @baud, @byte_size, @stop_bits, @parity, *args, **kwargs) @serial.read_timeout = SERIAL_READ_TIMEOUT if mode_init @@ -58,14 +58,6 @@ module Kiss else @exit_kiss = false end - - # Previous verious defaulted to Xastir-friendly configs. Unfortunately - # those don't work with Bluetooth TNCs, so we're reverting to None. - if kwargs - kwargs.each do |name, value| - write_setting(name, value) - end - end end public @@ -79,7 +71,7 @@ module Kiss if @serial == nil or @serial.closed? raise IOError.new('Attempting to close before the class has been started.') else - @serial.close + @serial.close(*args, **kwargs) end end end diff --git a/spec/kiss/kiss_abstract_spec.rb b/spec/kiss/kiss_abstract_spec.rb index 510e9d991e9e30fb422f36b246906d3a797f3c6d..d1990d721a748d4d6b8bebb527c61391846d52fb 100644 --- a/spec/kiss/kiss_abstract_spec.rb +++ b/spec/kiss/kiss_abstract_spec.rb @@ -1,15 +1,6 @@ require_relative '../../lib/kiss' require 'abstractify' -ENCODED_FRAME = [192, 0, 158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101, 192] - -DECODED_FRAME = { - :source => " W2GMD-1", - :destination => "OMG", - :payload => "test_encode_frame", - :path => ['WIDE1-1', 'WIDE2-2'] - } - class KissMock < Kiss::KissAbstract def initialize(strip_df_start=true) @@ -27,7 +18,7 @@ class KissMock < Kiss::KissAbstract end protected - def read_interface + def read_interface(*args, **kwargs) if @read_from_interface.length == 0 return nil end @@ -35,7 +26,7 @@ class KissMock < Kiss::KissAbstract end protected - def write_interface(data) + def write_interface(data, *args, **kwargs) @sent_to_interface << data end @@ -58,6 +49,9 @@ end describe Kiss::KissAbstract do + ENCODED_FRAME = [192, 0, 158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101, 192] + DECODED_FRAME = [158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101] + describe ".new" do context "Given a concrete child class" do it "is successfully instantiated" do @@ -79,7 +73,8 @@ describe Kiss::KissAbstract do let(:kiss_mock) {KissMock.new} it "successfully decoded and parsed the frame" do kiss_mock.add_read_from_interface(ENCODED_FRAME) - translated_frame = kiss_mock.read + translated_frame = kiss_mock.read_datagram + puts translated_frame.to_s expect(translated_frame).to eql(DECODED_FRAME) end end @@ -87,7 +82,7 @@ describe Kiss::KissAbstract do context "Given no bytes on the underlying interface" do let(:kiss_mock) {KissMock.new} it "successfully decoded and parsed the frame" do - translated_frame = kiss_mock.read + translated_frame = kiss_mock.read_datagram expect(translated_frame).to be_nil end end @@ -97,21 +92,11 @@ describe Kiss::KissAbstract do context "Given a frame as a map of strings" do let(:kiss_mock) {KissMock.new} it "successfully encodes to the underlying interface" do - kiss_mock.write(DECODED_FRAME) + kiss_mock.write_datagram(DECODED_FRAME) all_raw_frames = kiss_mock.get_sent_to_interface expect(all_raw_frames[0]).to eql(ENCODED_FRAME) end end - context "Given a frame that encodes but is invalid" do - let(:kiss_mock) {KissMock.new} - it "successfully encodes to the underlying interface" do - allow(Kiss::KissAbstract).to receive(:valid_frame).and_return(false) - - expect { - kiss_mock.write(DECODED_FRAME) - }.to raise_error(IOError) - end - end end end diff --git a/spec/kiss/kiss_serial_spec.rb b/spec/kiss/kiss_serial_spec.rb index bc5d08fb5f5fd014326238f8d2a6314cc6a93d88..01e8152f57f1274c9cdf95cc3dc5795f8263fa9b 100644 --- a/spec/kiss/kiss_serial_spec.rb +++ b/spec/kiss/kiss_serial_spec.rb @@ -2,16 +2,10 @@ require_relative '../../lib/kiss' require 'abstractify' require 'serialport' -ENCODED_FRAME = [192, 0, 158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101, 192] - -DECODED_FRAME = { - :source => "W2GMD-1", - :destination => "OMG", - :payload => "test_encode_frame", - :path => ['WIDE1-1', 'WIDE2-2'] - } - describe Kiss::KissSerial do + ENCODED_FRAME = [192, 0, 158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101, 192] + + DECODED_FRAME = [158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98, 64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100, 101, 95, 102, 114, 97, 109, 101] describe ".new" do context "Given valid arguments" do @@ -37,12 +31,6 @@ describe Kiss::KissSerial do expect(kiss_serial).to_not receive(:write_interface) kiss_serial.connect end - it "does not call write_setting" do - allow( SerialPort ).to receive(:new).and_return(serial_port) - allow(serial_port).to receive(:read_timeout=).with(-1).once - expect(kiss_serial).to_not receive(:write_setting) - kiss_serial.connect - end end context "Given an arbitrary string for mode_init, and no kwargs/extra arguments" do @@ -61,13 +49,6 @@ describe Kiss::KissSerial do allow(serial_port).to receive(:write).with(Kiss::MODE_INIT_W8DED.map { |b| b.chr }.join) kiss_serial.connect(Kiss::MODE_INIT_W8DED) end - it "calls write_setting" do - allow( SerialPort ).to receive(:new).and_return(serial_port) - allow(serial_port).to receive(:read_timeout=).with(-1).once - expect(kiss_serial).to_not receive(:write_setting) - allow(serial_port).to receive(:write).with(Kiss::MODE_INIT_W8DED.map { |b| b.chr }.join) - kiss_serial.connect(Kiss::MODE_INIT_W8DED) - end it "calls write on underlying serial device with proper values" do allow( SerialPort ).to receive(:new).and_return(serial_port) allow(serial_port).to receive(:read_timeout=).with(-1).once @@ -163,7 +144,7 @@ describe Kiss::KissSerial do kiss_serial.connect(Kiss::MODE_INIT_W8DED) expect(serial_port).to receive(:write).with(ENCODED_FRAME.map { |b| b.chr }.join).once - kiss_serial.write(DECODED_FRAME) + kiss_serial.write_datagram(DECODED_FRAME) end end end @@ -180,7 +161,7 @@ describe Kiss::KissSerial do kiss_serial.connect(Kiss::MODE_INIT_W8DED) expect(serial_port).to receive(:read).and_return(ENCODED_FRAME.map { |b| b.chr }.join, nil) - actual_frame = kiss_serial.read + actual_frame = kiss_serial.read_datagram expect(actual_frame).to eql(DECODED_FRAME) end end