| // Copyright 2021 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| /** Decoder class for decoding bytes using HDLC protocol */ |
| |
| import * as protocol from './protocol'; |
| import * as util from './util'; |
| |
| const _MIN_FRAME_SIZE = 6; // 1 B address + 1 B control + 4 B CRC-32 |
| |
| /** Indicates if an error occurred */ |
| export enum FrameStatus { |
| OK = 'OK', |
| FCS_MISMATCH = 'frame check sequence failure', |
| FRAMING_ERROR = 'invalid flag or escape characters', |
| BAD_ADDRESS = 'address field too long', |
| } |
| |
| /** |
| * A single HDLC frame |
| */ |
| export class Frame { |
| rawEncoded: Uint8Array; |
| rawDecoded: Uint8Array; |
| status: FrameStatus; |
| |
| address = -1; |
| control: Uint8Array = new Uint8Array(); |
| data: Uint8Array = new Uint8Array(); |
| |
| constructor( |
| rawEncoded: Uint8Array, |
| rawDecoded: Uint8Array, |
| status: FrameStatus = FrameStatus.OK |
| ) { |
| this.rawEncoded = rawEncoded; |
| this.rawDecoded = rawDecoded; |
| this.status = status; |
| |
| if (status === FrameStatus.OK) { |
| const [address, addressLength] = protocol.decodeAddress(rawDecoded); |
| if (addressLength === 0) { |
| this.status = FrameStatus.BAD_ADDRESS; |
| return; |
| } |
| this.address = address; |
| this.control = rawDecoded.slice(addressLength, addressLength + 1); |
| this.data = rawDecoded.slice(addressLength + 1, -4); |
| } |
| } |
| |
| /** |
| * True if this represents a valid frame. |
| * |
| * If false, then parsing failed. The status is set to indicate what type of |
| * error occurred, and the data field contains all bytes parsed from the frame |
| * (including bytes parsed as address or control bytes). |
| */ |
| ok(): boolean { |
| return this.status === FrameStatus.OK; |
| } |
| } |
| |
| enum DecoderState { |
| INTERFRAME, |
| FRAME, |
| FRAME_ESCAPE, |
| } |
| |
| /** Decodes one or more HDLC frames from a stream of data. */ |
| export class Decoder { |
| private decodedData = new Uint8Array(); |
| private rawData = new Uint8Array(); |
| private state = DecoderState.INTERFRAME; |
| |
| /** |
| * Decodes and yields HDLC frames, including corrupt frames |
| * |
| * The ok() method on Frame indicates whether it is valid or represents a |
| * frame parsing error. |
| * |
| * @yield Frames, which may be valid (frame.ok)) okr corrupt (!frame.ok()) |
| */ |
| *process(data: Uint8Array): IterableIterator<Frame> { |
| for (const byte of data) { |
| const frame = this.processByte(byte); |
| if (frame != null) { |
| yield frame; |
| } |
| } |
| } |
| |
| /** |
| * Decodes and yields valid HDLC frames, logging any errors. |
| * |
| * @yield Valid HDLC frames |
| */ |
| *processValidFrames(data: Uint8Array): IterableIterator<Frame> { |
| const frames = this.process(data); |
| for (const frame of frames) { |
| if (frame.ok()) { |
| yield frame; |
| } else { |
| console.warn( |
| 'Failed to decode frame: %s; discarded %d bytes', |
| frame.status, |
| frame.rawEncoded.length |
| ); |
| console.debug('Discarded data: %s', frame.rawEncoded); |
| } |
| } |
| } |
| |
| private checkFrame(data: Uint8Array): FrameStatus { |
| if (data.length < _MIN_FRAME_SIZE) { |
| return FrameStatus.FRAMING_ERROR; |
| } |
| const frameCrc = new DataView(data.slice(-4).buffer).getInt8(0); |
| const crc = new DataView( |
| protocol.frameCheckSequence(data.slice(0, -4)).buffer |
| ).getInt8(0); |
| if (crc !== frameCrc) { |
| return FrameStatus.FCS_MISMATCH; |
| } |
| return FrameStatus.OK; |
| } |
| |
| private finishFrame(status: FrameStatus): Frame { |
| const frame = new Frame( |
| new Uint8Array(this.rawData), |
| new Uint8Array(this.decodedData), |
| status |
| ); |
| this.rawData = new Uint8Array(); |
| this.decodedData = new Uint8Array(); |
| return frame; |
| } |
| |
| private processByte(byte: number): Frame | undefined { |
| let frame; |
| |
| // Record every byte except the flag character. |
| if (byte != protocol.FLAG) { |
| this.rawData = util.concatenate(this.rawData, Uint8Array.of(byte)); |
| } |
| |
| switch (this.state) { |
| case DecoderState.INTERFRAME: |
| if (byte === protocol.FLAG) { |
| if (this.rawData.length > 0) { |
| frame = this.finishFrame(FrameStatus.FRAMING_ERROR); |
| } |
| this.state = DecoderState.FRAME; |
| } |
| break; |
| |
| case DecoderState.FRAME: |
| if (byte == protocol.FLAG) { |
| if (this.rawData.length > 0) { |
| frame = this.finishFrame(this.checkFrame(this.decodedData)); |
| } |
| } else if (byte == protocol.ESCAPE) { |
| this.state = DecoderState.FRAME_ESCAPE; |
| } else { |
| this.decodedData = util.concatenate( |
| this.decodedData, |
| Uint8Array.of(byte) |
| ); |
| } |
| break; |
| |
| case DecoderState.FRAME_ESCAPE: |
| if (byte === protocol.FLAG) { |
| frame = this.finishFrame(FrameStatus.FRAMING_ERROR); |
| this.state = DecoderState.FRAME; |
| } else if (protocol.VALID_ESCAPED_BYTES.includes(byte)) { |
| this.state = DecoderState.FRAME; |
| this.decodedData = util.concatenate( |
| this.decodedData, |
| Uint8Array.of(protocol.escape(byte)) |
| ); |
| } else { |
| this.state = DecoderState.INTERFRAME; |
| } |
| break; |
| } |
| return frame; |
| } |
| } |