From 911ff0ead0954dcafaa47dbf18d8819b183a2cfe Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 18 May 2023 20:30:51 +0200 Subject: [PATCH 01/15] avclan: Initial revision. This includes two decoders, a generic IEBus decoder handling lower-level logic input and a higher level AVC-LAN which decodes Toyota-specific device addresses, functions and frame internals. --- decoders/avclan/__init__.py | 24 ++ decoders/avclan/lists.py | 242 +++++++++++++++ decoders/avclan/pd.py | 566 ++++++++++++++++++++++++++++++++++++ decoders/iebus/__init__.py | 26 ++ decoders/iebus/pd.py | 467 +++++++++++++++++++++++++++++ 5 files changed, 1325 insertions(+) create mode 100644 decoders/avclan/__init__.py create mode 100644 decoders/avclan/lists.py create mode 100644 decoders/avclan/pd.py create mode 100644 decoders/iebus/__init__.py create mode 100644 decoders/iebus/pd.py diff --git a/decoders/avclan/__init__.py b/decoders/avclan/__init__.py new file mode 100644 index 00000000..7eb5a88d --- /dev/null +++ b/decoders/avclan/__init__.py @@ -0,0 +1,24 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Maciej Grela +## +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . + + +''' +AVC-LAN is an IEBus variant used for multimedia communications inside Toyota vehicles +''' + +from .pd import Decoder diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py new file mode 100644 index 00000000..5deef9f7 --- /dev/null +++ b/decoders/avclan/lists.py @@ -0,0 +1,242 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Maciej Grela +## +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . + +''' +Lists of known values used by the AVC-LAN protocol in Toyota vehicles. + +The values here have been either copied from other reference sources, +and existing code: +- http://softservice.com.pl/corolla/avc/avclan.php +- https://github.com/halleysfifthinc/Toyota-AVC-LAN/ + +or reverse-engineered using a Head Unit and CD changer documented in detail on + +https://pop.fsck.pl/hardware/toyota-corolla.html +''' + +from enum import IntEnum, IntFlag + + +class Searchable: # pylint: disable=too-few-public-methods + '''A trait implementing value search in enums.''' + @classmethod + def has_value(cls, value): + '''Searches for a particular integer value in the enum''' + return value in iter(cls) + + +class Commands(Searchable, IntEnum): + ''' + Valid values for the control bits if IEBus frames. + + Reference: https://en.wikipedia.org/wiki/IEBus + Reference: http://softservice.com.pl/corolla/avc/avclan.php + ''' + READ_STATUS = 0x00 # Reads slave status + READ_DATA_LOCK = 0x03 # Reads data and locks unit + READ_LOCK_ADDR_LO = 0x04 # Reads lock address (lower 8 bits) + READ_LOCK_ADDR_HI = 0x05 # Reads lock address (higher 4 bits) + READ_STATUS_UNLOCK = 0x06 # Reads slave status and unlocks unit + READ_DATA = 0x07 # Reads data + WRITE_CMD_LOCK = 0x0a # Writes command and locks unit + WRITE_DATA_LOCK = 0x0b # Writes data and locks unit + WRITE_CMD = 0x0e # Writes command + WRITE_DATA = 0x0f # Writes data + + +class HWAddresses(Searchable, IntEnum): + ''' + Known hardware addresses. + ''' + EMV = 0x110 + AVX = 0x120 + DIN1_TV = 0x128 # 1DIN TV + AVN = 0x140 + G_BOOK = 0x144 # G-BOOK + AUDIO_HU1 = 0x160 # AUDIO H/U: Control Panel subassy + NAVI = 0x178 + MONET = 0x17C + TEL = 0x17D + Rr_TV = 0x180 # Rr-TV (sic!) pylint: disable=invalid-name + AUDIO_HU2 = 0x190 # AUDIO H/U: CD Player + Tuner + Audio Amplifier subassy + DVD_P = 0x1A0 + CLOCK = 0x1D6 + CAMERA_C = 0x1AC # CAMERA-C + Rr_CONT = 0x1C0 # Rr-CONT (sic!) pylint: disable=invalid-name + TV_TUNER2 = 0x1C2 # TV-TUNER2 + PANEL = 0x1C4 + GW = 0x1C6 # G/W + FM_M_LCD = 0x1C8 # FM-M-LCD + ST_WHEEL_CTRL = 0x1CC + GW_TRIP = 0x1D8 # G/W for Trip + BODY = 0x1EC + RADIO_TUNER = 0x1F0 + XM = 0x1F1 + SIRIUS = 0x1F2 + RSA = 0x1F4 + RSE = 0x1F6 + GROUP_AUDIO = 0x1FF # Group 1 - All Audio devices + TV_TUNER = 0x230 + CD_CH2 = 0x240 + DVD_CH = 0x250 + CAMERA = 0x280 + CD_CH1 = 0x360 + MD_CH = 0x3A0 + DSP_AMP = 0x440 + AMP = 0x480 + ETC = 0x530 + MAYDAY = 0x5C8 + BROADCAST = 0xFFF # General Broadcast (All devices on bus) + + +class FunctionIDs(Searchable, IntEnum): + ''' + These are called "Logical Addresses" in the Softservice site + but I think a better term would be "functions" + Reference: http://softservice.com.pl/corolla/avc/avclan.php + ''' + COMM_CTRL = 0x01 # communication ctrl + COMMUNICATION = 0x12 # communication + SW = 0x21 + SW_NAME = 0x23 # SW with name + SW_CONVERTING = 0x24 + CMD_SW = 0x25 # command SW + BEEP_HU = 0x28 # beep dev in HU + BEEP_SPEAKERS = 0x29 # beep via speakers + FRONT_PSNG_MONITOR = 0x34 # front passenger monitor + CD_CHANGER2 = 0x43 # Reported by CD_CH2 (0x240) + BLUETOOTH_TEL = 0x55 + INFO_DRAWING = 0x56 # information drawing + NAV_ECU = 0x58 # navigation ECU + CAMERA = 0x5C + CLIMATE_DRAWING = 0x5D # Climate ctrl drawing + AUDIO_DRAWING = 0x5E + TRIP_INFO_DRAWING = 0x5F + TUNER = 0x60 + TAPE_DECK = 0x61 + CD = 0x62 + CD_CHANGER = 0x63 # Reported by CD_CH1 (0x360) + AUDIO_AMP = 0x74 # Audio amplifier + GPS = 0x80 # GPS receiver + VOICE_CTRL = 0x85 # voice control + CLIMATE_CTRL_DEV = 0xE0 # climate ctrl dev + TRIP_INFO = 0xE5 + + +class CommCtrlOpcodes(Searchable, IntEnum): + ''' + Opcodes for the COMM_CTRL function. + + It looks like response opcode = request opcode + 0x10 but + this rule doesn't match all the opcodes seen. Specifically + the 0x45 code does not match. + ''' + LIST_FUNCTIONS_REQ = 0x00 + LIST_FUNCTIONS_RESP = 0x10 + ENDSCAN_REQ = 0x08 + ENDSCAN_RESP = 0x18 + PING_REQ = 0x0a + PING_RESP = 0x1a + STATE_REQ = 0x0c + STATE_RESP = 0x1c + REPORT_REQ = 0x20 + REPORT_RESP = 0x30 + ADVERTISE_FUNCTION = 0x45 # xx=60,61,62,63... function + GENERAL_QUERY = 0x46 # any device is use + + +class CDOpcodes(Searchable, IntEnum): + '''Opcodes for the CD Player function.''' + REPORT_PLAYBACK = 0xf1 + REPORT_TRACK_NAME = 0xfd + + +class CDStateCodes(Searchable, IntFlag): + '''State codes for the CD Player function.''' + OPEN = 0x01 + ERR1 = 0x02 + SEEKING = 0x08 + PLAYBACK = 0x10 + SEEKING_TRACK = 0x20 + LOAD = 0x80 + + +class CDFlags(Searchable, IntFlag): + '''Bit flags for the CD Player function.''' + DISK_RANDOM = 0x02 + RANDOM = 0x04 + DISK_REPEAT = 0x08 + REPEAT = 0x10 + DISK_SCAN = 0x20 + SCAN = 0x40 + + +class CmdSwOpcodes(Searchable, IntEnum): + '''Opcodes for the SW_CMD function.''' + EJECT = 0x80 + PWRVOL_KNOB_RIGHTHAND_TURN = 0x9c + PWRVOL_KNOB_LEFTHAND_TURN = 0x9d + TRACK_SEEK_UP = 0x94 + TRACK_SEEK_DOWN = 0x95 + CD_ENABLE_SCAN = 0xa6 + CD_DISABLE_SCAN = 0xa7 + CD_ENABLE_REPEAT = 0xa0 + CD_DISABLE_REPEAT = 0xa1 + CD_ENABLE_RANDOM = 0xb0 + CD_DISABLE_RANDOM = 0xb1 + + +class AudioAmpOpcodes(Searchable, IntEnum): + '''Opcodes for the AUDIO_AMP function.''' + REPORT = 0xf1 + + +class AudioAmpFlags(Searchable, IntFlag): + '''Flags for the AUDIO_AMP functions.''' + BIT0 = 0x01 + BIT1 = 0x02 + MUTE = 0x04 + + +class TunerOpcodes(Searchable, IntEnum): + '''Opcodes for the TUNER function.''' + REPORT = 0xf1 + + +class TunerFlags(Searchable, IntFlag): + '''Bit flags for the TUNER function.''' + AF = 0x40 + REG = 0x10 + TP = 0x04 + TA = 0x08 + + +class TunerState(Searchable, IntEnum): + '''States for the TUNER function.''' + ON = 0x01 + OFF = 0x00 + + +class TunerModes(Searchable, IntEnum): + '''Modes for the TUNER function.''' + MANUAL = 0x27 + AST_SEARCH = 0x0a + SCAN_DOWN = 0x07 + SCAN_UP = 0x06 + READY = 0x01 + OFF = 0x00 diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py new file mode 100644 index 00000000..3c0553cb --- /dev/null +++ b/decoders/avclan/pd.py @@ -0,0 +1,566 @@ +## +# This file is part of the libsigrokdecode project. +## +# Copyright (C) 2023 Maciej Grela +## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +## +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# pylint: disable=missing-module-docstring + +from collections import namedtuple +import sigrokdecode as srd # pylint: disable=import-error +from .lists import * # pylint: disable=wildcard-import,unused-wildcard-import + +DataByte = namedtuple('DataByte', ['b', 'ss', 'es']) + + +# Reference: https://docs.python.org/3/library/itertools.html +def first_true(iterable, default=None, pred=None): + """Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item + for which pred(item) is true. + + """ + # first_true([a,b,c], x) --> a or b or c or x + # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x + return next(filter(pred, iterable), default) + + +class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes + '''AVC-LAN Decoder class used by libsigrokdecode.''' + + api_version = 3 + id = 'avclan' + name = 'AVC-LAN' + longname = 'AVC-LAN Toyota Audio-Video Local Area Network' + desc = 'AVC-LAN Protocol Decoder (IEBus Mode 2 variant)' + license = 'gplv3+' + inputs = ['iebus'] + outputs = [] + tags = ['Automotive'] + channels = () + options = () + annotations = ( + ('address', 'Device Address'), # 0 + ('function', 'Function'), # 1 + + # Control Protocol + ('ctrl-opcode', 'Opcode'), # 2 + ('sequence-no', 'Sequence No.'), # 3 + ('advertised-function', 'Function'), # 4 + + # HU Commands + ('cmd-opcode', 'Opcode'), # 5 + + # CD Player + ('cd-opcode', 'Opcode'), # 6 + ('cd-state', 'State'), # 7 + ('cd-flags', 'Flags'), # 8 + ('disc-number', 'Disc Number'), # 9 + ('track-number', 'Track Number'), # 10 + ('disc-title', 'Disc Name'), # 11 + ('track-title', 'Track Name'), # 12 + ('playback-time', 'Playback time'), # 13 + + # Audio AMP + ('audio-opcode', 'Opcode'), # 14 + ('audio-flags', 'Audio Flags'), # 15 + ('volume', 'Volume'), # 16 + ('bass', 'Bass'), # 17 + ('treble', 'Treble'), # 18 + ('fade', 'Fade'), # 19 + ('balance', 'Balance'), # 20 + + # TUNER (radio) + ('radio-opcode', 'Opcode'), # 21 + ('radio-state', 'State'), # 22 + ('radio-mode', 'Mode'), # 23 + ('radio-flags', 'Flags'), # 24 + ('band', 'Band'), # 25 + ('channel', 'Channel'), # 26 + ('freq', 'Frequency'), # 27 + + ('warning', 'Warning') + ) + + annotation_rows = ( + ('devices', 'Device Addresses and Functions', (0, 1)), + ('control', 'Network Control', (2, 3, 4)), + ('cmd', 'HU Commands', (5,)), + ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13)), + ('audio', 'Audio Amplifier', (14, 15, 16, 17, 18, 19, 20)), + ('radio', 'Radio Tuner', (21, 22, 23, 24, 25, 26, 27)), + ('warnings', 'Warnings', (28,)) + ) + + + def __init__(self): + # Make pylint happy (attribute-defined-outside-init checker) + self.state = None + self.broadcast_bit = None + self.master_addr = self.slave_addr = None + self.control = None + self.data_length = self.data_bytes = None + self.from_function = self.to_function = None + self.samplerate = None + self.out_ann = None + + self.reset() + + + def reset(self): + '''Reset decoder state.''' + self.state = 'IDLE' + self.broadcast_bit = None + self.master_addr = self.slave_addr = None + self.control = None + self.data_length = self.data_bytes = None + self.from_function = self.to_function = None + + + def start(self): + '''Start decoder.''' + self.out_ann = self.register(srd.OUTPUT_ANN) + + + def metadata(self, key, value): + '''Handle metadata input from libsigrokdecode.''' + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + + def find_annotation(self, anno_name: str): + '''Find an annotation index based on its name.''' + return first_true(enumerate(self.annotations), default=(None, (None, None)), + pred=lambda item: item[1][0] == anno_name) + + + def putx(self, anno_name: str, ss, es, v): + '''Put in an annotation using its name.''' + (idx, _) = self.find_annotation(anno_name) + if idx is None: + raise RuntimeError(f'Cannot find annotation name {anno_name}') + self.put(ss, es, self.out_ann, [idx, v]) + + + def pkt_to_01(self): + '''Handle frames to function 01(COMM_CTRL).''' + self.pkt_comm_ctrl() + + + def pkt_from_01(self): + '''Handle frames from function 01(COMM_CTRL).''' + self.pkt_comm_ctrl() + + + def pkt_comm_ctrl(self): + '''Handle control protocol frames.''' + opcode = self.data_bytes[0].b + opcode_anno = f'{opcode:02x}' + + if CommCtrlOpcodes.has_value(opcode): + opcode = CommCtrlOpcodes(opcode) + + self.putx('ctrl-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, + [f'Opcode: {opcode.name}', opcode.name] + ) + + if opcode == CommCtrlOpcodes.ADVERTISE_FUNCTION: + logic_id = self.data_bytes[1].b + if FunctionIDs.has_value(logic_id): + logic_id = FunctionIDs(logic_id) + self.putx('advertised-function', + self.data_bytes[1].ss, self.data_bytes[1].ss, + [f'Function: {logic_id.name}', logic_id.name] + ) + + elif opcode == CommCtrlOpcodes.PING_REQ: + sequence = self.data_bytes[1].b + self.putx('sequence-no', self.data_bytes[1].ss, self.data_bytes[1].es, + [f'Sequence Number: {sequence}', str(sequence), 'Seq']) + + elif opcode == CommCtrlOpcodes.PING_RESP: + sequence = self.data_bytes[1].b + self.putx('sequence-no', self.data_bytes[1].ss, self.data_bytes[1].es, + [f'Sequence Number: {sequence}', str(sequence), 'Seq']) + + elif opcode == CommCtrlOpcodes.LIST_FUNCTIONS_RESP: + for (idx, logical_addr) in enumerate([d.b for d in self.data_bytes[1:]], 1): + anno = f'{logical_addr:02x}' + if FunctionIDs.has_value(logical_addr): + logical_addr = FunctionIDs(logical_addr) + anno = logical_addr.name + + self.putx('advertised-function', + self.data_bytes[idx].ss, self.data_bytes[idx].es, + [f'Function: {anno}', anno, 'Func']) + + + def pkt_from_25(self): + '''Handle frames from function 25(CMD_SW).''' + opcode = self.data_bytes[0].b + if CmdSwOpcodes.has_value(opcode): + opcode = CmdSwOpcodes(opcode) + + self.putx('cmd-opcode', self.data_bytes[0].ss, self.data_bytes[0].ss, + [f'Opcode: {opcode.name}', opcode.name] + ) + + + def pkt_from_60(self): + '''Handle frames from function 60(TUNER).''' + opcode = self.data_bytes[0].b + + if TunerOpcodes.has_value(opcode): + opcode = TunerOpcodes(opcode) + + self.putx('radio-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, + [f'Opcode: {opcode.name}', opcode.name] + ) + + if opcode == TunerOpcodes.REPORT: + tuner_state = self.data_bytes[1].b + if TunerState.has_value(tuner_state): + tuner_state = TunerState(tuner_state) + + self.putx('radio-state', self.data_bytes[1].ss, self.data_bytes[1].es, + [f'State: {tuner_state.name}', tuner_state.name]) + + tuner_mode = self.data_bytes[2].b + if TunerModes.has_value(tuner_mode): + tuner_mode = TunerModes(tuner_mode) + + self.putx('radio-mode', self.data_bytes[2].ss, self.data_bytes[2].es, + [f'Mode: {tuner_mode.name}', tuner_mode.name, 'Mode']) + + # Did not know how to properly implement this with Python enums + band_type = self.data_bytes[3].b & 0xF0 + band_number = self.data_bytes[3].b & 0x0F + if band_type == 0x80: + band_type = 'FM' + freq_start = 87.5 + freq_step = 0.05 + freq_unit = 'MHz' + elif band_type == 0xC0: + # Long Wave broadcast band + band_type = 'AM' + freq_start = 153 + freq_step = 1 + freq_unit = 'kHz' + elif band_type == 0x00: + # Medium Wave broadcast band + band_type = 'AM' + freq_start = 522 + freq_step = 9 + freq_unit = 'kHz' + + self.putx('band', self.data_bytes[3].ss, self.data_bytes[3].es, + [f'Band: {band_type} {band_number}', + f'{band_type} {band_number}', 'Band']) + + # Frequency is 2 bytes big-endian + freq = 256 * self.data_bytes[4].b + self.data_bytes[5].b + freq = freq_start + (freq-1) * freq_step + self.putx('freq', self.data_bytes[4].ss, self.data_bytes[5].es, + [f'Freq: {freq} {freq_unit}', f'{freq} {freq_unit}', 'Freq']) + + channel = self.data_bytes[6].b + if channel > 0: + self.putx('channel', self.data_bytes[6].ss, self.data_bytes[6].es, + [f'CH #{channel}', 'CH']) + + flags1 = TunerFlags(self.data_bytes[7].b) + self.putx('radio-flags', self.data_bytes[7].ss, self.data_bytes[7].es, + [f'Flags: {str(flags1)}', 'Flags', 'F']) + flags2 = TunerFlags(self.data_bytes[8].b) + self.putx('radio-flags', self.data_bytes[8].ss, self.data_bytes[8].es, + [f'Flags: {str(flags2)}', 'Flags', 'F']) + + + def bcd2dec(self, b: int): + '''Convert a BCD encoded byte to a decimal integer.''' + return 10 * ((b & 0xF0) >> 4) + (b & 0x0F) + + + def pkt_from_62(self): + '''Handle frames from function 62(CD).''' + self.pkt_from_cd_player() + + + def pkt_from_63(self): + '''Handle frames from function 63(CD_CHANGER).''' + self.pkt_from_cd_player() + + + def pkt_from_cd_player(self): + '''Handle frames from a CD player.''' + opcode = self.data_bytes[0].b + opcode_anno = f'{opcode:02x}' + + if CDOpcodes.has_value(opcode): + opcode = CDOpcodes(opcode) + opcode_anno = opcode.name + + if opcode == CDOpcodes.REPORT_PLAYBACK: + cd_state = self.data_bytes[2].b + anno = f'{cd_state:02x}' + if CDStateCodes.has_value(cd_state): + cd_state = CDStateCodes(cd_state) + anno = cd_state.name + + self.putx('cd-state', self.data_bytes[2].ss, self.data_bytes[2].es, + [f'State: {anno}', anno, 'State']) + + # This is always 0x01 for builtin CD player + disc_number = self.data_bytes[3].b + anno = [ 'CD #', 'CD', 'C' ] + if disc_number != 0xff: + anno.insert(0, f'CD #{disc_number}') + + self.putx('disc-number', self.data_bytes[3].ss, self.data_bytes[3].es, anno) + + track_number = self.data_bytes[4].b + anno = [ 'Track #', 'Tra', 'T' ] + if track_number != 0xff: + anno.insert(0, f'Track #{track_number}') + + self.putx('track-number', self.data_bytes[4].ss, self.data_bytes[4].es, anno) + + minutes = self.data_bytes[5].b + seconds = self.data_bytes[6].b + anno = ['Time', 'T'] + if minutes != 0xff and seconds != 0xff: + minutes = self.bcd2dec(minutes) + seconds = self.bcd2dec(seconds) + anno.insert(0, f'{minutes:02d}:{seconds:02d}') + anno.insert(0, f'Time: {minutes:02d}:{seconds:02d}') + + self.putx('playback-time', self.data_bytes[5].ss, self.data_bytes[6].es, anno) + + cd_flags = self.data_bytes[7].b + anno = str(cd_flags) + if CDFlags.has_value(cd_flags): + cd_flags = CDFlags(cd_flags) + anno = cd_flags.name + + self.putx('cd-flags', self.data_bytes[7].ss, self.data_bytes[7].es, + [f'Flags: {anno}', anno, 'Flags']) + + elif opcode == CDOpcodes.REPORT_TRACK_NAME: + disc_number = self.data_bytes[1].b + track_number = self.data_bytes[2].b + text = ''.join([chr(d.b) for d in self.data_bytes[5:]]) + if disc_number != 0xff: + self.putx('disc-number', self.data_bytes[1].ss, self.data_bytes[1].es, + [f'CD #{disc_number}', 'CD #']) + self.putx('track-number', self.data_bytes[2].ss, self.data_bytes[2].es, + [f'Track #{track_number}', 'Track #']) + self.putx('track-title', self.data_bytes[5].ss, self.data_bytes[-1].es, + [f'Title: {text}', 'Title']) + + self.putx('cd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, + [f'Opcode: {opcode_anno}', opcode_anno, 'Opcode']) + + + def map_left_right(self, value: int, center: int, + negative_tag: str = '-', positive_tag: str = '+'): + ''' + Map values corresponding to left/right or front/back settings to strings. + Used for balance, fade and so on. + ''' + value -= center + if value < 0: + return f'{negative_tag}{abs(value)}' + if value > 0: + return f'{positive_tag}{abs(value)}' + return '0' + + + def pkt_74(self): + '''Handle frames to/from function 74(AUDIO_AMP)''' + opcode = self.data_bytes[0].b + + if AudioAmpOpcodes.has_value(opcode): + opcode = AudioAmpOpcodes(opcode) + + self.putx('audio-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, + [f'Opcode: {opcode.name}', opcode.name] + ) + + if opcode == AudioAmpOpcodes.REPORT: + # First byte is always 0x80 in volume reports + volume = self.data_bytes[2].b + self.putx('volume', self.data_bytes[2].ss, self.data_bytes[2].es, + [f'Volume: {volume}', 'Volume', 'Vol']) + + balance = self.map_left_right( + self.data_bytes[3].b, 0x10, negative_tag='L', positive_tag='R') + self.putx('balance', self.data_bytes[3].ss, self.data_bytes[3].es, + [f'Balance: {balance}', 'Balance', 'Bal']) + + fade = self.map_left_right( + self.data_bytes[4].b, 0x10, negative_tag='F', positive_tag='R') + self.putx('fade', self.data_bytes[4].ss, self.data_bytes[4].es, + [f'Fade: {fade}', 'Fade']) + + bass = self.map_left_right(self.data_bytes[5].b, 0x10) + self.putx('bass', self.data_bytes[5].ss, self.data_bytes[5].es, + [f'Bass: {bass}', 'Bass']) + + treble = self.map_left_right(self.data_bytes[7].b, 0x10) + self.putx('treble', self.data_bytes[7].ss, self.data_bytes[7].es, + [f'Treble: {treble}', 'Treble']) + + flags = AudioAmpFlags(self.data_bytes[12].b) + self.putx('audio-flags', self.data_bytes[12].ss, self.data_bytes[12].es, + [f'Flags: {str(flags)}', 'Flags']) + + + def decode(self, ss, es, data): + '''Decode Python output data from low-level (iebus) decoder.''' + + (ptype, pdata) = data + + if ptype == 'NAK': + # A NAK condition has been observed, bus is reset back to IDLE + # + self.reset() + + if self.state == 'IDLE' and ptype == 'HEADER': + self.broadcast_bit = pdata + self.state = 'MASTER ADDRESS' + elif self.state == 'MASTER ADDRESS' and ptype == 'MASTER ADDRESS': + (address, parity_bit) = pdata + self.master_addr = address + if HWAddresses.has_value(self.master_addr): + self.putx('address', ss, es, [ + HWAddresses(self.master_addr).name]) + + self.state = 'SLAVE ADDRESS' + + elif self.state == 'SLAVE ADDRESS' and ptype == 'SLAVE ADDRESS': + (address, parity_bit, ack_bit) = pdata + self.slave_addr = address + + if HWAddresses.has_value(self.slave_addr): + self.putx('address', ss, es, [ + HWAddresses(self.slave_addr).name]) + + self.state = 'CONTROL' + + elif self.state == 'CONTROL' and ptype == 'CONTROL': + (control, parity_bit, ack_bit) = pdata + self.control = control + + self.state = 'DATA LENGTH' + elif self.state == 'DATA LENGTH' and ptype == 'DATA LENGTH': + (data_length, parity_bit, ack_bit) = pdata + + self.data_length = data_length + + self.state = 'DATA' + elif self.state == 'DATA' and ptype == 'DATA': + + # Ignore parity bit and ack bit for simplicity + self.data_bytes = [DataByte(b=b, ss=ss, es=es) + for (b, parity_bit, ack_bit, ss, es) in pdata] + + # + # Decode logical device IDs + # + if self.broadcast_bit == 1: + # Unicast packets + + # Some packets do not seem to follow this format + if len(self.data_bytes) >= 3: + + # In unicast communications the meaning of the first + # data byte is unknown. Values seen so far are: + # - 0x00 + # - 0xff + # + self.from_function = self.data_bytes[1].b + self.to_function = self.data_bytes[2].b + + from_anno = ['From Function', 'From'] + if FunctionIDs.has_value(self.from_function): + self.from_function = FunctionIDs(self.from_function) + from_anno.insert( + 0, f'From Function: {self.from_function.name}') + self.putx( + 'function', self.data_bytes[1].ss, self.data_bytes[1].es, from_anno) + + to_anno = ['To Function', 'To'] + if FunctionIDs.has_value(self.to_function): + self.to_function = FunctionIDs(self.to_function) + to_anno.insert( + 0, f'To Function: {self.to_function.name}') + self.putx( + 'function', self.data_bytes[2].ss, self.data_bytes[2].es, to_anno) + + self.data_bytes = self.data_bytes[3:] + + elif self.broadcast_bit == 0: + # Broadcast packets + + self.from_function = self.data_bytes[0].b + self.to_function = self.data_bytes[1].b + + from_anno = ['From Function', 'From'] + if FunctionIDs.has_value(self.from_function): + self.from_function = FunctionIDs(self.from_function) + from_anno.insert( + 0, f'From Function: {self.from_function.name}') + self.putx( + 'function', self.data_bytes[0].ss, self.data_bytes[0].es, from_anno) + + to_anno = ['To Function', 'To'] + if FunctionIDs.has_value(self.to_function): + self.to_function = FunctionIDs(self.to_function) + to_anno.insert(0, f'To Function: {self.to_function.name}') + self.putx( + 'function', self.data_bytes[1].ss, self.data_bytes[1].es, to_anno) + + self.data_bytes = self.data_bytes[2:] + + else: + raise RuntimeError( + f'Unexpected broadcast bit value {self.broadcast_bit}') + + # Dispatch to device and function ID handling + # This logic allows for prioritised matching + fn = first_true([ + getattr( + self, f'pkt_from_{self.from_function:02x}_to_{self.to_function:02x}', None), + getattr(self, f'pkt_to_{self.to_function:02x}', None), + getattr(self, f'pkt_from_{self.from_function:02x}', None), + getattr(self, f'pkt_{self.to_function:02x}', None), + getattr(self, f'pkt_{self.from_function:02x}', None), + # getattr(self, f'pkt_default', None) + ]) + + if fn: + fn() + + # + # Prepare for next frame + self.reset() + + else: + # + # Invalid state transition + self.reset() diff --git a/decoders/iebus/__init__.py b/decoders/iebus/__init__.py new file mode 100644 index 00000000..e6e9e220 --- /dev/null +++ b/decoders/iebus/__init__.py @@ -0,0 +1,26 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Maciej Grela +## +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . + + +''' +IEBus (Inter Equipment Bus) is a communication bus specification +"between equipments within a vehicle or a chassis". IEBus is mainly +used for car audio and car navigations as well some vending machines. +''' + +from .pd import Decoder diff --git a/decoders/iebus/pd.py b/decoders/iebus/pd.py new file mode 100644 index 00000000..fed2abfd --- /dev/null +++ b/decoders/iebus/pd.py @@ -0,0 +1,467 @@ +# +# This file is part of the libsigrokdecode project. +# +# Copyright (C) 2023 Maciej Grela +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +IEBus data timings help to understand the bits() method. + +This drawning documents Mode 2 +Drawing not to scale +Bit 0 dominates on the bus + + + │ │ + byte n-1 │ byte n │ byte n+1 + ◄───────── │ ◄───────── │ ─────────► + │ │ + │ prep. synchronization data │ prep. + │ period period period │ period + │ │ + │ │ │ │ │ + ──────────┼────────┼─────────────────────┼────────┼────────┼───────────────────────────────► + │ │ │ │ │ + + ▲ ΔV + │ │ + │ ┌─────────────────────┐ │ ┌────────────── │ > 120 mV + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ │ Bus voltage +Bit 1 │ │ │ │ │ │ (differential) + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ + ───────────────┘ └────────┼────────┘ │ < 20 mV + │ + │ │ │ + │ │ │ + │ │ │ + │ │ │ + │ │ │ ▲ ΔV + │ │ + │ ┌──────────────────────────────┐ ┌────────────── │ > 120 mV + │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ │ Bus voltage +Bit 0 │ │ │ │ │ │ (differential) + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ + ───────────────┘ │ └────────┘ │ < 20 mV + │ + │ │ │ │ │ + ───────────┼────────┼─────────────────────┼────────┼────────┼────────────────────────────► t + │ │ │ │ │ + 7 µs 20 µs 12 µs 7 µs + + +The IEBus decoder uses the following OUTPUT_PYTHON format: + +Frame: +[, ] + +: +- 'HEADER' (Start bit + Broadcast bit, is the Broadcast bit value) +- 'MASTER ADDRESS' ( is (address, parity_bit) ) +- 'SLAVE ADDRESS' ( is (address, parity_bit, ack_bit) ) +- 'CONTROL' ( is (control, parity_bit, ack_bit) ) +- 'DATA LENGTH' ( is (data_length, parity_bit, ack_bit) ) +- 'DATA' ( contains frame data bytes and their ss/es numbers formatted + as follows: [ (data1, parity_bit, ack_bit, ss, es), + (data2, parity_bit, ack_bit, ss, es), ... + ] ) +- 'NAK' ( is None) + +Parity errors and NAK conditions are annotated but otherwise all data +is passed to the output unchanged. +Control bits are either decoded to one of the names from the Commands enum or +left as integer if no match is found. +''' + +from functools import reduce +from enum import IntEnum +import sigrokdecode as srd # pylint: disable=import-error + + +class Commands(IntEnum): + ''' + Valid values for the control bits if IEBus frames. + + Reference: https://en.wikipedia.org/wiki/IEBus + Reference: http://softservice.com.pl/corolla/avc/avclan.php + ''' + + + @classmethod + def has_value(cls, value: int): + '''Searches for a particular integer value in the enum''' + return value in iter(cls) + + + READ_STATUS = 0x00 # Reads slave status + READ_DATA_LOCK = 0x03 # Reads data and locks unit + READ_LOCK_ADDR_LO = 0x04 # Reads lock address (lower 8 bits) + READ_LOCK_ADDR_HI = 0x05 # Reads lock address (higher 4 bits) + READ_STATUS_UNLOCK = 0x06 # Reads slave status and unlocks unit + READ_DATA = 0x07 # Reads data + WRITE_CMD_LOCK = 0x0a # Writes command and locks unit + WRITE_DATA_LOCK = 0x0b # Writes data and locks unit + WRITE_CMD = 0x0e # Writes command + WRITE_DATA = 0x0f # Writes data + + +def first_true(iterable, default=None, pred=None): + """Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item + for which pred(item) is true. + + Reference: https://docs.python.org/3/library/itertools.html + """ + # first_true([a,b,c], x) --> a or b or c or x + # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x + return next(filter(pred, iterable), default) + + +class Decoder(srd.Decoder): + '''IEBus decoder class for usage by libsigrokdecode.''' + + api_version = 3 + id = 'iebus' + name = 'IEBus' + longname = 'Inter-Equipment Bus' + desc = 'Inter-Equipment Bus is an automotive communication bus used in Toyota and Honda vehicles' # pylint: disable=line-too-long + license = 'gplv3+' + inputs = ['logic'] + outputs = ['iebus'] + tags = ['Automotive'] + channels = ( + {'id': 'bus', 'name': 'BUS', 'desc': 'Bus input'}, + ) + options = ( + {'id': 'mode', 'desc': 'Mode', 'values': ( + 'Mode 2', ), 'default': 'Mode 2'}, + ) + annotations = ( + ('start-bit', 'Start bit'), # 0 + ('bit', 'Bit'), # 1 + ('parity', 'Parity'), # 2 + ('ack', 'Acknowledge'), # 3 + + ('broadcast', 'Broadcast flag'), # 4 + ('maddr', 'Master address'), # 5 + ('saddr', 'Slave address'), # 6 + ('control', 'Control'), # 7 + ('datalen', 'Data Length'), # 8 + ('byte', 'Data Byte'), # 9 + + ('warning', 'Warning') + ) + + annotation_rows = ( + ('bits', 'Bits', (0, 1, 2, 3)), + ('fields', 'Raw Fields', (4, 5, 6, 7, 8, 9)), + ('warnings', 'Warnings', (10,)) + ) + + + def __init__(self): + self.out_ann = self.out_python = None + self.samplerate = None + self.broadcast_bit = None + self.bits_begin = self.bits_end = None + + + def reset(self): + '''Reset decoder state.''' + + + def start(self): + '''Start decoder.''' + self.out_ann = self.register(srd.OUTPUT_ANN) + self.out_python = self.register(srd.OUTPUT_PYTHON) + + + def metadata(self, key, value): + '''Handle metadata input from libsigrokdecode.''' + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + + def reduce_bus(self, bus: list): + '''Reduce a list of bit values to an integer (MSB bit order).''' + return reduce(lambda a, b: (a << 1) | b, bus) + + + def bits(self, n: int): + ''' + Read n bits from the bus. + Only Mode 2 is currently supported. + + + ''' + self.bits_begin = None + self.bits_end = None + + bits = [] + while n > 0: + self.wait({0: 'r'}) + if self.bits_begin is None: + self.bits_begin = self.samplenum + bit_start = self.samplenum + + # In Mode 2 synchronization phase is 20µs, data phase is 13µs, + # sample bit state 27µs after the synchronization edge + # (approx. 20µs + 13µs / 2) + + pins = self.wait({'skip': int(27e-6 * self.samplerate)}) + bit = (pins[0] + 1) % 2 + + # Assume full 33µs bit length after sync edge + bit_end = bit_start + int(33e-6 * self.samplerate) + + bits.append(bit) + self.put(bit_start, bit_end, self.out_ann, + [1, [str(bit)]]) + + self.bits_end = bit_end + + n -= 1 + + return bits + + + def bit(self): + '''Read one bit from the bus.''' + return self.bits(1)[0] + + + def value(self, num_bits: int): + '''Read a value from the bus having num_bits bits (MSB first)''' + v = self.reduce_bus(self.bits(num_bits)) + return (v, self.bits_begin, self.bits_end) + + + def find_annotation(self, anno_name: str): + '''Find an annotation index based on its name.''' + return first_true(enumerate(self.annotations), default=(None, (None, None)), + pred=lambda item: item[1][0] == anno_name) + + + def putx(self, anno_name: str, ss, es, v): + '''Put in an annotation using its name.''' + (idx, _) = self.find_annotation(anno_name) + if idx is None: + raise RuntimeError(f'Cannot find annotation name {anno_name}') + self.put(ss, es, self.out_ann, [idx, v]) + + def header(self): + ''' + Read the header from the bus and add appropriate annotations. + Returns the header bits. + ''' + + # Start bit + # + self.wait({0: 'r'}) + ss = self.samplenum + self.wait({0: 'f'}) + es = self.samplenum + + if (es - ss) / self.samplerate < 100e-6: + self.putx('warning', ss, es, + ['Startbit too short', 'Too short']) + + return (None, None, ss, es) + + self.putx('start-bit', ss, es, ['Start bit', 'Start', 'S']) + + # Broadcast bit + # + broadcast_bit = self.read_broadcast_bit() + es = self.samplenum + + return (1, broadcast_bit, ss, es) + + + def read_broadcast_bit(self): + '''Read the broadcast bit from the bus and add appropriate annotations.''' + broadcast_bit = self.bit() + + if broadcast_bit == 1: + broadcast_anno = ['Unicast', 'Uni', 'U'] + elif broadcast_bit == 0: + # Broadcast traffic has bit 0 here in order to + # dominate on the bus. + broadcast_anno = ['Broadcast', 'Bro', 'B'] + else: + raise RuntimeError(f'Unexpected broadcast bit value {broadcast_bit}') + + self.putx('broadcast', self.bits_begin, self.bits_end, broadcast_anno) + + return broadcast_bit + + + def ack_bit(self): + '''Read the ACK/NAK bit from the bus and add appropriate annotations.''' + ack_bit = self.bit() + if self.broadcast_bit == 1: + # Non-broadcast traffic + if ack_bit == 0: + # ACK needs to dominate on the bus + self.putx('ack', self.bits_begin, self.bits_end, ['ACK', 'A']) + elif ack_bit == 1: + self.putx('ack', self.bits_begin, self.bits_end, ['NAK', 'N']) + else: + raise RuntimeError('Unexpected value {ack_bit} for the acknowledge bit') + + return ack_bit + + + def parity_bit(self, value: int): + '''Read the parity bit from the bus and add appropriate annotations.''' + parity_bit = self.bit() + self.putx('parity', self.bits_begin, + self.bits_end, ['Parity', 'Par', 'P']) + expected_parity = bin(value).count('1') % 2 + if expected_parity != parity_bit: + self.putx('warning', self.bits_begin, + self.bits_end, ['Parity error']) + + return parity_bit + + + def handle_data_bytes(self, data_len: int): + ''' + Read a specified amount of data bytes from the bus, add appropriate + annotations, record sample counts for each byte and return a list. + ''' + data_bytes = [] + + while data_len > 0: + (b, ss, es) = self.value(8) + + self.putx('byte', ss, es, + [f'Data: 0x{b:02x}', f'0x{b:02x}']) + + parity_bit = self.parity_bit(b) + ack_bit = self.ack_bit() + + data_bytes.append((b, parity_bit, ack_bit, ss, es)) + + data_len -= 1 + + # We don't care about the value of these bits, just annotations + if self.broadcast_bit == 1 and ack_bit == 1: + # NAK condition, restart search for start bit + break + + return data_bytes + + + def decode(self): + '''Decode samples, main function called by the libsigrokdecode framework''' + while True: + + (start_bit, broadcast_bit, ss, es) = self.header() + + if start_bit is None: + # Header was not valid, search for next one + continue + + # Store broadcast bit for later checks of NAK conditions + self.broadcast_bit = broadcast_bit + + self.put(ss, es, self.out_python, ['HEADER', self.broadcast_bit]) + + # Master adddress + # + (master_addr, ss, es) = self.value(12) + + self.putx('maddr', ss, es, [ f'Master: 0x{master_addr:03x}', f'0x{master_addr:03x}' ]) + + parity_bit = self.parity_bit(master_addr) + self.put(ss, es, self.out_python, [ + 'MASTER ADDRESS', (master_addr, parity_bit) ]) + + # Slave adddress + # + (slave_addr, ss, es) = self.value(12) + + self.putx('saddr', ss, es, [ f'Slave: 0x{slave_addr:03x}', f'0x{slave_addr:03x}']) + + parity_bit = self.parity_bit(slave_addr) + ack_bit = self.ack_bit() + self.put(ss, es, self.out_python, [ + 'SLAVE ADDRESS', (slave_addr, parity_bit, ack_bit) + ]) + if self.broadcast_bit == 1 and ack_bit == 1: + # NAK condition, restart search for start bit + self.put(self.bits_begin, self.bits_end, self.out_python, ['NAK', None]) + continue + + # Control bits + # + (control, ss, es) = self.value(4) + + if Commands.has_value(control): + control = Commands(control) + self.putx('control', ss, es, [ f'Control: {control.name}', f'{control.name}' ]) + else: + self.putx('control', ss, es, [ f'Control: 0x{control:01x}' ]) + + parity_bit = self.parity_bit(control.value) + ack_bit = self.ack_bit() + self.put(ss, es, self.out_python, [ 'CONTROL', (control.name, parity_bit, ack_bit) ]) + + if self.broadcast_bit == 1 and ack_bit == 1: + # NAK condition, restart search for start bit + self.put(self.bits_begin, self.bits_end, self.out_python, ['NAK', None]) + continue + + # Data length + # + (data_len, ss, es) = self.value(8) + + if data_len == 0: # 0x00 is 256 bytes + data_len = 256 + + self.putx('datalen', ss, es, [ f'Data Length: {data_len}', f'{data_len}', 'Len' ]) + + if data_len > 128: + self.putx('warning', ss, es, + ['Message too long, mode 2 allows only for 128 bytes maximum', + 'Message too long', 'Too long']) + + parity_bit = self.parity_bit(data_len) + ack_bit = self.ack_bit() + self.put(ss, es, self.out_python, [ 'DATA LENGTH', (data_len, parity_bit, ack_bit) ]) + + if self.broadcast_bit == 1 and ack_bit == 1: + # NAK condition, restart search for start bit + self.put(self.bits_begin, self.bits_end, self.out_python, ['NAK', None]) + continue + + # Data bytes + # + ss = self.samplenum + data_bytes = self.handle_data_bytes(data_len) + es = self.samplenum + + self.put(ss, es, self.out_python, [ 'DATA', data_bytes ]) From 31cc90f31856b95ac5410a68acad329300b0e8dd Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Wed, 31 May 2023 22:45:54 +0200 Subject: [PATCH 02/15] iebus: Fix handling of parity for unknown field values --- decoders/iebus/pd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decoders/iebus/pd.py b/decoders/iebus/pd.py index fed2abfd..ff34f8ed 100644 --- a/decoders/iebus/pd.py +++ b/decoders/iebus/pd.py @@ -419,6 +419,7 @@ def decode(self): # Control bits # (control, ss, es) = self.value(4) + parity_bit = self.parity_bit(control) if Commands.has_value(control): control = Commands(control) @@ -426,7 +427,6 @@ def decode(self): else: self.putx('control', ss, es, [ f'Control: 0x{control:01x}' ]) - parity_bit = self.parity_bit(control.value) ack_bit = self.ack_bit() self.put(ss, es, self.out_python, [ 'CONTROL', (control.name, parity_bit, ack_bit) ]) @@ -438,6 +438,7 @@ def decode(self): # Data length # (data_len, ss, es) = self.value(8) + parity_bit = self.parity_bit(data_len) if data_len == 0: # 0x00 is 256 bytes data_len = 256 @@ -449,7 +450,6 @@ def decode(self): ['Message too long, mode 2 allows only for 128 bytes maximum', 'Message too long', 'Too long']) - parity_bit = self.parity_bit(data_len) ack_bit = self.ack_bit() self.put(ss, es, self.out_python, [ 'DATA LENGTH', (data_len, parity_bit, ack_bit) ]) From a0f4936ee43ae191e48cd01aebbfa38104335e1c Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Wed, 31 May 2023 23:06:54 +0200 Subject: [PATCH 03/15] avclan: Properly annotate CMD_SW opcodes and advertise function fields --- decoders/avclan/pd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 3c0553cb..0917c223 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -184,7 +184,7 @@ def pkt_comm_ctrl(self): if FunctionIDs.has_value(logic_id): logic_id = FunctionIDs(logic_id) self.putx('advertised-function', - self.data_bytes[1].ss, self.data_bytes[1].ss, + self.data_bytes[1].ss, self.data_bytes[1].es, [f'Function: {logic_id.name}', logic_id.name] ) @@ -216,7 +216,7 @@ def pkt_from_25(self): if CmdSwOpcodes.has_value(opcode): opcode = CmdSwOpcodes(opcode) - self.putx('cmd-opcode', self.data_bytes[0].ss, self.data_bytes[0].ss, + self.putx('cmd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, [f'Opcode: {opcode.name}', opcode.name] ) From b51522db8b9b0d823eb9e1b9bc754c7aa30954d7 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Wed, 31 May 2023 23:08:01 +0200 Subject: [PATCH 04/15] avclan: Call function-based handlers only when functions are defined --- decoders/avclan/pd.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 0917c223..213bf182 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -541,20 +541,22 @@ def decode(self, ss, es, data): raise RuntimeError( f'Unexpected broadcast bit value {self.broadcast_bit}') - # Dispatch to device and function ID handling - # This logic allows for prioritised matching - fn = first_true([ - getattr( - self, f'pkt_from_{self.from_function:02x}_to_{self.to_function:02x}', None), - getattr(self, f'pkt_to_{self.to_function:02x}', None), - getattr(self, f'pkt_from_{self.from_function:02x}', None), - getattr(self, f'pkt_{self.to_function:02x}', None), - getattr(self, f'pkt_{self.from_function:02x}', None), - # getattr(self, f'pkt_default', None) - ]) - - if fn: - fn() + if self.from_function is not None and + self.to_function is not None: + # Dispatch to device and function ID handling + # This logic allows for prioritised matching + fn = first_true([ + getattr( + self, f'pkt_from_{self.from_function:02x}_to_{self.to_function:02x}', None), + getattr(self, f'pkt_to_{self.to_function:02x}', None), + getattr(self, f'pkt_from_{self.from_function:02x}', None), + getattr(self, f'pkt_{self.to_function:02x}', None), + getattr(self, f'pkt_{self.from_function:02x}', None), + # getattr(self, f'pkt_default', None) + ]) + + if fn: + fn() # # Prepare for next frame From 9000122d0c9746ed4bcade14ccd4c4560d122e3c Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 00:07:11 +0200 Subject: [PATCH 05/15] avclan: Allow for multiple handlers per frame, improve decoding of CD-related frames --- decoders/avclan/lists.py | 15 +++++++++-- decoders/avclan/pd.py | 55 +++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index 5deef9f7..c0397e1e 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -156,16 +156,27 @@ class CommCtrlOpcodes(Searchable, IntEnum): STATE_RESP = 0x1c REPORT_REQ = 0x20 REPORT_RESP = 0x30 + + # Used when HU is switching between Radio and CD + DISABLE_FUNCTION_REQ = 0x43 + DISABLE_FUNCTION_RESP = 0x53 + ENABLE_FUNCTION_REQ = 0x42 + ENABLE_FUNCTION_RESP = 0x52 + ADVERTISE_FUNCTION = 0x45 # xx=60,61,62,63... function GENERAL_QUERY = 0x46 # any device is use class CDOpcodes(Searchable, IntEnum): '''Opcodes for the CD Player function.''' + # Events + INSERTED_CD = 0x50 + REMOVED_CD = 0x51 + + # Reports REPORT_PLAYBACK = 0xf1 REPORT_TRACK_NAME = 0xfd - class CDStateCodes(Searchable, IntFlag): '''State codes for the CD Player function.''' OPEN = 0x01 @@ -173,7 +184,7 @@ class CDStateCodes(Searchable, IntFlag): SEEKING = 0x08 PLAYBACK = 0x10 SEEKING_TRACK = 0x20 - LOAD = 0x80 + LOADING = 0x80 class CDFlags(Searchable, IntFlag): diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 213bf182..cff789b3 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -25,21 +25,6 @@ DataByte = namedtuple('DataByte', ['b', 'ss', 'es']) -# Reference: https://docs.python.org/3/library/itertools.html -def first_true(iterable, default=None, pred=None): - """Returns the first true value in the iterable. - - If no true value is found, returns *default* - - If *pred* is not None, returns the first item - for which pred(item) is true. - - """ - # first_true([a,b,c], x) --> a or b or c or x - # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x - return next(filter(pred, iterable), default) - - class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes '''AVC-LAN Decoder class used by libsigrokdecode.''' @@ -157,6 +142,16 @@ def putx(self, anno_name: str, ss, es, v): self.put(ss, es, self.out_ann, [idx, v]) + def pkt_from_12(self): + '''Handle frames from function 12(COMMUNICATION)''' + self.pkt_comm_ctrl() + + + def pkt_to_12(self): + '''Handle frames to function 12(COMMUNICATION)''' + self.pkt_comm_ctrl() + + def pkt_to_01(self): '''Handle frames to function 01(COMM_CTRL).''' self.pkt_comm_ctrl() @@ -208,6 +203,9 @@ def pkt_comm_ctrl(self): self.putx('advertised-function', self.data_bytes[idx].ss, self.data_bytes[idx].es, [f'Function: {anno}', anno, 'Func']) + return True + + return False def pkt_from_25(self): @@ -219,6 +217,7 @@ def pkt_from_25(self): self.putx('cmd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, [f'Opcode: {opcode.name}', opcode.name] ) + return False def pkt_from_60(self): @@ -290,6 +289,10 @@ def pkt_from_60(self): self.putx('radio-flags', self.data_bytes[8].ss, self.data_bytes[8].es, [f'Flags: {str(flags2)}', 'Flags', 'F']) + return True + + return False + def bcd2dec(self, b: int): '''Convert a BCD encoded byte to a decimal integer.''' @@ -310,6 +313,7 @@ def pkt_from_cd_player(self): '''Handle frames from a CD player.''' opcode = self.data_bytes[0].b opcode_anno = f'{opcode:02x}' + ret = False if CDOpcodes.has_value(opcode): opcode = CDOpcodes(opcode) @@ -320,7 +324,7 @@ def pkt_from_cd_player(self): anno = f'{cd_state:02x}' if CDStateCodes.has_value(cd_state): cd_state = CDStateCodes(cd_state) - anno = cd_state.name + anno = str(cd_state) self.putx('cd-state', self.data_bytes[2].ss, self.data_bytes[2].es, [f'State: {anno}', anno, 'State']) @@ -372,8 +376,11 @@ def pkt_from_cd_player(self): self.putx('track-title', self.data_bytes[5].ss, self.data_bytes[-1].es, [f'Title: {text}', 'Title']) + ret = True + self.putx('cd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, [f'Opcode: {opcode_anno}', opcode_anno, 'Opcode']) + return ret def map_left_right(self, value: int, center: int, @@ -429,6 +436,10 @@ def pkt_74(self): self.putx('audio-flags', self.data_bytes[12].ss, self.data_bytes[12].es, [f'Flags: {str(flags)}', 'Flags']) + return True + + return False + def decode(self, ss, es, data): '''Decode Python output data from low-level (iebus) decoder.''' @@ -541,11 +552,10 @@ def decode(self, ss, es, data): raise RuntimeError( f'Unexpected broadcast bit value {self.broadcast_bit}') - if self.from_function is not None and - self.to_function is not None: + if (self.from_function is not None) and (self.to_function is not None): # Dispatch to device and function ID handling # This logic allows for prioritised matching - fn = first_true([ + fn = filter(lambda x: x is not None, [ getattr( self, f'pkt_from_{self.from_function:02x}_to_{self.to_function:02x}', None), getattr(self, f'pkt_to_{self.to_function:02x}', None), @@ -555,8 +565,11 @@ def decode(self, ss, es, data): # getattr(self, f'pkt_default', None) ]) - if fn: - fn() + for f in fn: + v = f() + print(f'{f}: {v}') + if v is True: + break # # Prepare for next frame From dd86e4c4a5d71b03f147f51f9ad9656a7b68e5d5 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 12:58:36 +0200 Subject: [PATCH 06/15] avclan: Restore code removed by mistake, don't print handler results --- decoders/avclan/pd.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index cff789b3..86e3d15b 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -25,6 +25,21 @@ DataByte = namedtuple('DataByte', ['b', 'ss', 'es']) +# Reference: https://docs.python.org/3/library/itertools.html +def first_true(iterable, default=None, pred=None): + """Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item + for which pred(item) is true. + + """ + # first_true([a,b,c], x) --> a or b or c or x + # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x + return next(filter(pred, iterable), default) + + class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes '''AVC-LAN Decoder class used by libsigrokdecode.''' @@ -566,9 +581,7 @@ def decode(self, ss, es, data): ]) for f in fn: - v = f() - print(f'{f}: {v}') - if v is True: + if f() is True: break # From 744286b9cbb3d2c20264572ff174f7ba954c602e Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 13:53:03 +0200 Subject: [PATCH 07/15] avclan: Don't pass frame to function decoders if there is no data left to decode with them --- decoders/avclan/pd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 86e3d15b..d70d8586 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -567,7 +567,8 @@ def decode(self, ss, es, data): raise RuntimeError( f'Unexpected broadcast bit value {self.broadcast_bit}') - if (self.from_function is not None) and (self.to_function is not None): + if (self.from_function is not None) and (self.to_function is not None) and \ + (len(self.data_bytes) > 0): # Dispatch to device and function ID handling # This logic allows for prioritised matching fn = filter(lambda x: x is not None, [ From 93263e19bf36e2db682a9264e1585a37d0781cd1 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 13:53:51 +0200 Subject: [PATCH 08/15] iebus: Properly handle unknown CONTROL field values and CONTROL ACK bit --- decoders/iebus/pd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/decoders/iebus/pd.py b/decoders/iebus/pd.py index ff34f8ed..4c6897b4 100644 --- a/decoders/iebus/pd.py +++ b/decoders/iebus/pd.py @@ -420,15 +420,15 @@ def decode(self): # (control, ss, es) = self.value(4) parity_bit = self.parity_bit(control) + ack_bit = self.ack_bit() if Commands.has_value(control): control = Commands(control) self.putx('control', ss, es, [ f'Control: {control.name}', f'{control.name}' ]) - else: - self.putx('control', ss, es, [ f'Control: 0x{control:01x}' ]) + self.put(ss, es, self.out_python, [ 'CONTROL', (control.name, parity_bit, ack_bit) ]) - ack_bit = self.ack_bit() - self.put(ss, es, self.out_python, [ 'CONTROL', (control.name, parity_bit, ack_bit) ]) + self.putx('control', ss, es, [ f'Control: 0x{control:02x}', f'0x{control:02x}' ]) + self.put(ss, es, self.out_python, [ 'CONTROL', (control, parity_bit, ack_bit) ]) if self.broadcast_bit == 1 and ack_bit == 1: # NAK condition, restart search for start bit From d0fc9d9ced83ed3c600f4f2ff14d121df9aea5e6 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 14:22:21 +0200 Subject: [PATCH 09/15] iebus: Forgotten else --- decoders/iebus/pd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decoders/iebus/pd.py b/decoders/iebus/pd.py index 4c6897b4..5aae4641 100644 --- a/decoders/iebus/pd.py +++ b/decoders/iebus/pd.py @@ -426,9 +426,9 @@ def decode(self): control = Commands(control) self.putx('control', ss, es, [ f'Control: {control.name}', f'{control.name}' ]) self.put(ss, es, self.out_python, [ 'CONTROL', (control.name, parity_bit, ack_bit) ]) - - self.putx('control', ss, es, [ f'Control: 0x{control:02x}', f'0x{control:02x}' ]) - self.put(ss, es, self.out_python, [ 'CONTROL', (control, parity_bit, ack_bit) ]) + else: + self.putx('control', ss, es, [ f'Control: 0x{control:02x}', f'0x{control:02x}' ]) + self.put(ss, es, self.out_python, [ 'CONTROL', (control, parity_bit, ack_bit) ]) if self.broadcast_bit == 1 and ack_bit == 1: # NAK condition, restart search for start bit From 48e70ae6aafbb6cb68768be9a8d064d29cea9529 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Thu, 1 Jun 2023 16:26:46 +0200 Subject: [PATCH 10/15] avclan: Add unknown bits and rework to decode all flags and states properly --- decoders/avclan/lists.py | 17 +++++++++++++++-- decoders/avclan/pd.py | 11 ++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index c0397e1e..c4f403c1 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -181,20 +181,24 @@ class CDStateCodes(Searchable, IntFlag): '''State codes for the CD Player function.''' OPEN = 0x01 ERR1 = 0x02 + BIT2 = 0x04 SEEKING = 0x08 PLAYBACK = 0x10 SEEKING_TRACK = 0x20 + BIT6 = 0x40 LOADING = 0x80 class CDFlags(Searchable, IntFlag): '''Bit flags for the CD Player function.''' + BIT0 = 0x01 DISK_RANDOM = 0x02 RANDOM = 0x04 DISK_REPEAT = 0x08 REPEAT = 0x10 DISK_SCAN = 0x20 SCAN = 0x40 + BIT7 = 0x80 class CmdSwOpcodes(Searchable, IntEnum): @@ -222,6 +226,11 @@ class AudioAmpFlags(Searchable, IntFlag): BIT0 = 0x01 BIT1 = 0x02 MUTE = 0x04 + BIT3 = 0x08 + BIT4 = 0x10 + BIT5 = 0x20 + BIT6 = 0x40 + BIT7 = 0x80 class TunerOpcodes(Searchable, IntEnum): @@ -231,10 +240,14 @@ class TunerOpcodes(Searchable, IntEnum): class TunerFlags(Searchable, IntFlag): '''Bit flags for the TUNER function.''' - AF = 0x40 - REG = 0x10 + BIT0 = 0x01 + BIT1 = 0x02 TP = 0x04 TA = 0x08 + REG = 0x10 + BIT5 = 0x20 + AF = 0x40 + BIT7 = 0x80 class TunerState(Searchable, IntEnum): diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index d70d8586..62c025bd 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -336,11 +336,8 @@ def pkt_from_cd_player(self): if opcode == CDOpcodes.REPORT_PLAYBACK: cd_state = self.data_bytes[2].b - anno = f'{cd_state:02x}' - if CDStateCodes.has_value(cd_state): - cd_state = CDStateCodes(cd_state) - anno = str(cd_state) + anno = str(CDStateCodes(cd_state)) self.putx('cd-state', self.data_bytes[2].ss, self.data_bytes[2].es, [f'State: {anno}', anno, 'State']) @@ -371,11 +368,7 @@ def pkt_from_cd_player(self): self.putx('playback-time', self.data_bytes[5].ss, self.data_bytes[6].es, anno) cd_flags = self.data_bytes[7].b - anno = str(cd_flags) - if CDFlags.has_value(cd_flags): - cd_flags = CDFlags(cd_flags) - anno = cd_flags.name - + anno = str(CDFlags(cd_flags)) self.putx('cd-flags', self.data_bytes[7].ss, self.data_bytes[7].es, [f'Flags: {anno}', anno, 'Flags']) From d9e1049c8bbe3639c6cc5ac3ed01e9cede479b86 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Wed, 7 Jun 2023 13:48:35 +0200 Subject: [PATCH 11/15] avclan: Add parsing of disc slots and loader status frames --- decoders/avclan/lists.py | 11 ++++++++ decoders/avclan/pd.py | 60 ++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index c4f403c1..a49994c1 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -175,8 +175,19 @@ class CDOpcodes(Searchable, IntEnum): # Reports REPORT_PLAYBACK = 0xf1 + REPORT_LOADER = 0xf3 REPORT_TRACK_NAME = 0xfd + +class CDSlots(Searchable, IntFlag): + SLOT1 = 0x01 + SLOT2 = 0x02 + SLOT3 = 0x04 + SLOT4 = 0x08 + SLOT5 = 0x10 + SLOT6 = 0x20 + + class CDStateCodes(Searchable, IntFlag): '''State codes for the CD Player function.''' OPEN = 0x01 diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 62c025bd..5942db16 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -75,24 +75,25 @@ class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes ('disc-title', 'Disc Name'), # 11 ('track-title', 'Track Name'), # 12 ('playback-time', 'Playback time'), # 13 + ('disc-slots', 'Disc Slots'), # 14 # Audio AMP - ('audio-opcode', 'Opcode'), # 14 - ('audio-flags', 'Audio Flags'), # 15 - ('volume', 'Volume'), # 16 - ('bass', 'Bass'), # 17 - ('treble', 'Treble'), # 18 - ('fade', 'Fade'), # 19 - ('balance', 'Balance'), # 20 + ('audio-opcode', 'Opcode'), # 15 + ('audio-flags', 'Audio Flags'), # 16 + ('volume', 'Volume'), # 17 + ('bass', 'Bass'), # 18 + ('treble', 'Treble'), # 19 + ('fade', 'Fade'), # 20 + ('balance', 'Balance'), # 21 # TUNER (radio) - ('radio-opcode', 'Opcode'), # 21 - ('radio-state', 'State'), # 22 - ('radio-mode', 'Mode'), # 23 - ('radio-flags', 'Flags'), # 24 - ('band', 'Band'), # 25 - ('channel', 'Channel'), # 26 - ('freq', 'Frequency'), # 27 + ('radio-opcode', 'Opcode'), # 22 + ('radio-state', 'State'), # 23 + ('radio-mode', 'Mode'), # 24 + ('radio-flags', 'Flags'), # 25 + ('band', 'Band'), # 26 + ('channel', 'Channel'), # 27 + ('freq', 'Frequency'), # 28 ('warning', 'Warning') ) @@ -101,10 +102,10 @@ class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes ('devices', 'Device Addresses and Functions', (0, 1)), ('control', 'Network Control', (2, 3, 4)), ('cmd', 'HU Commands', (5,)), - ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13)), - ('audio', 'Audio Amplifier', (14, 15, 16, 17, 18, 19, 20)), - ('radio', 'Radio Tuner', (21, 22, 23, 24, 25, 26, 27)), - ('warnings', 'Warnings', (28,)) + ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13, 14)), + ('audio', 'Audio Amplifier', (15, 16, 17, 18, 19, 20, 21)), + ('radio', 'Radio Tuner', (22, 23, 24, 25, 26, 27, 28)), + ('warnings', 'Warnings', (29,)) ) @@ -324,6 +325,11 @@ def pkt_from_63(self): self.pkt_from_cd_player() + def pkt_from_43(self): + '''Handle frames from function 43(CD_CHANGER2)''' + self.pkt_from_cd_player() + + def pkt_from_cd_player(self): '''Handle frames from a CD player.''' opcode = self.data_bytes[0].b @@ -384,6 +390,24 @@ def pkt_from_cd_player(self): self.putx('track-title', self.data_bytes[5].ss, self.data_bytes[-1].es, [f'Title: {text}', 'Title']) + elif opcode == CDOpcodes.REPORT_LOADER: + + slots = self.data_bytes[2].b + anno = str(CDSlots(slots)) + self.putx('disc-slots', self.data_bytes[2].ss, self.data_bytes[2].es, + [f'Slots-1: {anno}', anno, 'Slots-1']) + + slots = self.data_bytes[4].b + anno = str(CDSlots(slots)) + self.putx('disc-slots', self.data_bytes[4].ss, self.data_bytes[4].es, + [f'Slots-2: {anno}', anno, 'Slots-2']) + + slots = self.data_bytes[6].b + anno = str(CDSlots(slots)) + self.putx('disc-slots', self.data_bytes[6].ss, self.data_bytes[6].es, + [f'Slots-3: {anno}', anno, 'Slots-3']) + + ret = True self.putx('cd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, From a749192c4180dd09f81d43b2c0339b4078522092 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Fri, 16 Jun 2023 20:04:51 +0200 Subject: [PATCH 12/15] avclan: Add decoding for a the REPORT_TOC frame and clarify cd slot fields This adds support for a report frame sent out from the CD Changer when CDs are changed and reports basic information from the CD TOC such as the total number of tracks as well as total playback time. Additionally, a second REPORT_TUNER variant opcode has been identified. --- decoders/avclan/lists.py | 4 ++ decoders/avclan/pd.py | 87 +++++++++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index a49994c1..1beb13b6 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -176,6 +176,8 @@ class CDOpcodes(Searchable, IntEnum): # Reports REPORT_PLAYBACK = 0xf1 REPORT_LOADER = 0xf3 + REPORT_LOADER2 = 0xf4 + REPORT_TOC = 0xf9 REPORT_TRACK_NAME = 0xfd @@ -215,6 +217,8 @@ class CDFlags(Searchable, IntFlag): class CmdSwOpcodes(Searchable, IntEnum): '''Opcodes for the SW_CMD function.''' EJECT = 0x80 + DISC_UP = 0x90 + DISC_DOWN = 0x91 PWRVOL_KNOB_RIGHTHAND_TURN = 0x9c PWRVOL_KNOB_LEFTHAND_TURN = 0x9d TRACK_SEEK_UP = 0x94 diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 5942db16..91f769bf 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -72,28 +72,29 @@ class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes ('cd-flags', 'Flags'), # 8 ('disc-number', 'Disc Number'), # 9 ('track-number', 'Track Number'), # 10 - ('disc-title', 'Disc Name'), # 11 - ('track-title', 'Track Name'), # 12 - ('playback-time', 'Playback time'), # 13 - ('disc-slots', 'Disc Slots'), # 14 + ('track-count', 'Track Count'), # 11 + ('disc-title', 'Disc Name'), # 12 + ('track-title', 'Track Name'), # 13 + ('playback-time', 'Playback time'), # 14 + ('disc-slots', 'Disc Slots'), # 15 # Audio AMP - ('audio-opcode', 'Opcode'), # 15 - ('audio-flags', 'Audio Flags'), # 16 - ('volume', 'Volume'), # 17 - ('bass', 'Bass'), # 18 - ('treble', 'Treble'), # 19 - ('fade', 'Fade'), # 20 - ('balance', 'Balance'), # 21 + ('audio-opcode', 'Opcode'), # 16 + ('audio-flags', 'Audio Flags'), # 17 + ('volume', 'Volume'), # 18 + ('bass', 'Bass'), # 19 + ('treble', 'Treble'), # 20 + ('fade', 'Fade'), # 21 + ('balance', 'Balance'), # 22 # TUNER (radio) - ('radio-opcode', 'Opcode'), # 22 - ('radio-state', 'State'), # 23 - ('radio-mode', 'Mode'), # 24 - ('radio-flags', 'Flags'), # 25 - ('band', 'Band'), # 26 - ('channel', 'Channel'), # 27 - ('freq', 'Frequency'), # 28 + ('radio-opcode', 'Opcode'), # 23 + ('radio-state', 'State'), # 24 + ('radio-mode', 'Mode'), # 25 + ('radio-flags', 'Flags'), # 26 + ('band', 'Band'), # 27 + ('channel', 'Channel'), # 28 + ('freq', 'Frequency'), # 29 ('warning', 'Warning') ) @@ -102,10 +103,10 @@ class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes ('devices', 'Device Addresses and Functions', (0, 1)), ('control', 'Network Control', (2, 3, 4)), ('cmd', 'HU Commands', (5,)), - ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13, 14)), - ('audio', 'Audio Amplifier', (15, 16, 17, 18, 19, 20, 21)), - ('radio', 'Radio Tuner', (22, 23, 24, 25, 26, 27, 28)), - ('warnings', 'Warnings', (29,)) + ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13, 14, 15)), + ('audio', 'Audio Amplifier', (16, 17, 18, 19, 20, 21, 22)), + ('radio', 'Radio Tuner', (23, 24, 25, 26, 27, 29)), + ('warnings', 'Warnings', (30,)) ) @@ -354,7 +355,7 @@ def pkt_from_cd_player(self): anno.insert(0, f'CD #{disc_number}') self.putx('disc-number', self.data_bytes[3].ss, self.data_bytes[3].es, anno) - + track_number = self.data_bytes[4].b anno = [ 'Track #', 'Tra', 'T' ] if track_number != 0xff: @@ -390,23 +391,55 @@ def pkt_from_cd_player(self): self.putx('track-title', self.data_bytes[5].ss, self.data_bytes[-1].es, [f'Title: {text}', 'Title']) - elif opcode == CDOpcodes.REPORT_LOADER: + elif opcode == CDOpcodes.REPORT_LOADER or opcode == CDOpcodes.REPORT_LOADER2: slots = self.data_bytes[2].b anno = str(CDSlots(slots)) self.putx('disc-slots', self.data_bytes[2].ss, self.data_bytes[2].es, - [f'Slots-1: {anno}', anno, 'Slots-1']) + [f'Available: {anno}', anno, 'Avail']) slots = self.data_bytes[4].b anno = str(CDSlots(slots)) self.putx('disc-slots', self.data_bytes[4].ss, self.data_bytes[4].es, - [f'Slots-2: {anno}', anno, 'Slots-2']) + [f'Disc Present: {anno}', anno, 'Pres']) slots = self.data_bytes[6].b anno = str(CDSlots(slots)) self.putx('disc-slots', self.data_bytes[6].ss, self.data_bytes[6].es, - [f'Slots-3: {anno}', anno, 'Slots-3']) + [f'Slot-3: {anno}', anno, 'Pres']) + + elif opcode == CDOpcodes.REPORT_TOC: + disc_number = self.data_bytes[1].b + anno = [ 'CD #', 'CD', 'C' ] + if disc_number != 0xff: + anno.insert(0, f'CD #{disc_number}') + + self.putx('disc-number', self.data_bytes[1].ss, self.data_bytes[1].es, anno) + + track_number = self.data_bytes[2].b + anno = [ 'Track #', 'Tra', 'T' ] + if track_number != 0xff: + anno.insert(0, f'Track #{track_number}') + + self.putx('track-number', self.data_bytes[2].ss, self.data_bytes[2].es, anno) + + track_count = self.data_bytes[3].b + anno = [ 'Track Count', 'Count', 'Cnt' ] + if track_count != 0xff: + anno.insert(0, f'Track Count: {track_count}') + + self.putx('track-count', self.data_bytes[3].ss, self.data_bytes[3].es, anno) + + minutes = self.data_bytes[4].b + seconds = self.data_bytes[5].b + anno = ['Total Time', 'T'] + if minutes != 0xff and seconds != 0xff: + minutes = self.bcd2dec(minutes) + seconds = self.bcd2dec(seconds) + anno.insert(0, f'{minutes:02d}:{seconds:02d}') + anno.insert(0, f'Total Time: {minutes:02d}:{seconds:02d}') + self.putx('playback-time', self.data_bytes[4].ss, self.data_bytes[5].es, anno) ret = True From c0dbcc68dd8acf79eeb0cc2857a23a161beebb2d Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Mon, 19 Jun 2023 19:36:40 +0200 Subject: [PATCH 13/15] iebus: Add support for bus polarity option and an option to ignore NAK conditions New options have been added that are useful for monitoring Tx lines where ACK condition does not occur as well as an option to reverse the bus polarity. --- decoders/iebus/pd.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/decoders/iebus/pd.py b/decoders/iebus/pd.py index 5aae4641..3d50f60b 100644 --- a/decoders/iebus/pd.py +++ b/decoders/iebus/pd.py @@ -157,6 +157,10 @@ class Decoder(srd.Decoder): options = ( {'id': 'mode', 'desc': 'Mode', 'values': ( 'Mode 2', ), 'default': 'Mode 2'}, + {'id': 'bus_polarity', 'desc': 'Bus polarity', 'default': 'idle-low', + 'values': ('idle-low', 'idle-high')}, + {'id': 'ignore_nak', 'desc': 'Ignore NAK condition', 'default': 'Disabled', + 'values': ('Disabled', 'Enabled')} ) annotations = ( ('start-bit', 'Start bit'), # 0 @@ -221,7 +225,13 @@ def bits(self, n: int): bits = [] while n > 0: - self.wait({0: 'r'}) + if self.options['bus_polarity'] == 'idle-low': + self.wait({0: 'r'}) + elif self.options['bus_polarity'] == 'idle-high': + self.wait({0: 'f'}) + else: + raise Exception(f'Unexpected bus_polarity value "{bus_polarity}"') + if self.bits_begin is None: self.bits_begin = self.samplenum bit_start = self.samplenum @@ -233,6 +243,10 @@ def bits(self, n: int): pins = self.wait({'skip': int(27e-6 * self.samplerate)}) bit = (pins[0] + 1) % 2 + # Invert bit value when bus is idle high + if self.options['bus_polarity'] == 'idle-high': + bit = (bit + 1) % 2 + # Assume full 33µs bit length after sync edge bit_end = bit_start + int(33e-6 * self.samplerate) @@ -279,10 +293,18 @@ def header(self): # Start bit # - self.wait({0: 'r'}) - ss = self.samplenum - self.wait({0: 'f'}) - es = self.samplenum + if self.options['bus_polarity'] == 'idle-low': + self.wait({0: 'r'}) + ss = self.samplenum + self.wait({0: 'f'}) + es = self.samplenum + elif self.options['bus_polarity'] == 'idle-high': + self.wait({0: 'f'}) + ss = self.samplenum + self.wait({0: 'r'}) + es = self.samplenum + else: + raise Exception(f'Unexpected bus_polarity value "{bus_polarity}"') if (es - ss) / self.samplerate < 100e-6: self.putx('warning', ss, es, @@ -323,6 +345,11 @@ def ack_bit(self): ack_bit = self.bit() if self.broadcast_bit == 1: # Non-broadcast traffic + + # Force ack bit to be 0 if NAK condition is to be ignored + if self.options['ignore_nak'] == 'Enabled': + ack_bit = 0 + if ack_bit == 0: # ACK needs to dominate on the bus self.putx('ack', self.bits_begin, self.bits_end, ['ACK', 'A']) From 47fb5ec484f33697bca249f070a8f8bab9ba57d3 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Sun, 25 Jun 2023 17:35:35 +0200 Subject: [PATCH 14/15] avclan: Add decoding of the frame used to request a disc title or track title reports --- decoders/avclan/lists.py | 3 +++ decoders/avclan/pd.py | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index 1beb13b6..45009939 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -173,6 +173,9 @@ class CDOpcodes(Searchable, IntEnum): INSERTED_CD = 0x50 REMOVED_CD = 0x51 + # Requests + REQUEST_TRACK_NAME = 0xed + # Reports REPORT_PLAYBACK = 0xf1 REPORT_LOADER = 0xf3 diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 91f769bf..57fc81be 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -105,7 +105,7 @@ class Decoder(srd.Decoder): # pylint: disable=too-many-instance-attributes ('cmd', 'HU Commands', (5,)), ('cd', 'CD Player', (6, 7, 8, 9, 10, 11, 12, 13, 14, 15)), ('audio', 'Audio Amplifier', (16, 17, 18, 19, 20, 21, 22)), - ('radio', 'Radio Tuner', (23, 24, 25, 26, 27, 29)), + ('radio', 'Radio Tuner', (23, 24, 25, 26, 27, 28, 29)), ('warnings', 'Warnings', (30,)) ) @@ -321,6 +321,10 @@ def pkt_from_62(self): self.pkt_from_cd_player() + def pkt_to_62(self): + self.pkt_to_cd_player() + + def pkt_from_63(self): '''Handle frames from function 63(CD_CHANGER).''' self.pkt_from_cd_player() @@ -331,6 +335,40 @@ def pkt_from_43(self): self.pkt_from_cd_player() + def pkt_to_cd_player(self): + '''Handle frames to a CD player.''' + opcode = self.data_bytes[0].b + opcode_anno = f'{opcode:02x}' + ret = False + + if CDOpcodes.has_value(opcode): + opcode = CDOpcodes(opcode) + opcode_anno = opcode.name + + if opcode == CDOpcodes.REQUEST_TRACK_NAME: + + # This is always 0x01 for builtin CD player + disc_number = self.data_bytes[1].b + anno = [ 'CD #', 'CD', 'C' ] + if disc_number != 0xff: + anno.insert(0, f'CD #{disc_number}') + + self.putx('disc-number', self.data_bytes[1].ss, self.data_bytes[1].es, anno) + + track_number = self.data_bytes[2].b + anno = [ 'Track #', 'Tra', 'T' ] + if track_number != 0xff: + anno.insert(0, f'Track #{track_number}') + + self.putx('track-number', self.data_bytes[2].ss, self.data_bytes[2].es, anno) + + ret = True + + self.putx('cd-opcode', self.data_bytes[0].ss, self.data_bytes[0].es, + [f'Opcode: {opcode_anno}', opcode_anno, 'Opcode']) + return ret + + def pkt_from_cd_player(self): '''Handle frames from a CD player.''' opcode = self.data_bytes[0].b From 27bd94af980cd1fb6c1091eb8a4467d40822a6c1 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Tue, 27 Jun 2023 14:49:10 +0200 Subject: [PATCH 15/15] avclan: Clarify names for opcodes used in LAN CHECK mode, document new report requests --- decoders/avclan/lists.py | 27 +++++++++++++++++---------- decoders/avclan/pd.py | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/decoders/avclan/lists.py b/decoders/avclan/lists.py index 45009939..6b6d3196 100644 --- a/decoders/avclan/lists.py +++ b/decoders/avclan/lists.py @@ -148,14 +148,15 @@ class CommCtrlOpcodes(Searchable, IntEnum): ''' LIST_FUNCTIONS_REQ = 0x00 LIST_FUNCTIONS_RESP = 0x10 - ENDSCAN_REQ = 0x08 - ENDSCAN_RESP = 0x18 - PING_REQ = 0x0a - PING_RESP = 0x1a - STATE_REQ = 0x0c - STATE_RESP = 0x1c - REPORT_REQ = 0x20 - REPORT_RESP = 0x30 + RESTART_LAN = 0x01 + LANCHECK_END_REQ = 0x08 + LANCHECK_END_RESP = 0x18 + LANCHECK_SCAN_REQ = 0x0a + LANCHECK_SCAN_RESP = 0x1a + LANCHECK_REQ = 0x0c + LANCHECK_RESP = 0x1c + PING_REQ = 0x20 + PING_RESP = 0x30 # Used when HU is switching between Radio and CD DISABLE_FUNCTION_REQ = 0x43 @@ -168,18 +169,24 @@ class CommCtrlOpcodes(Searchable, IntEnum): class CDOpcodes(Searchable, IntEnum): - '''Opcodes for the CD Player function.''' + ''' + Opcodes for the CD Player function. + These seem to be also common for the CD Changer function. + ''' # Events INSERTED_CD = 0x50 REMOVED_CD = 0x51 # Requests + REQUEST_PLAYBACK2 = 0xe2 + REQUEST_LOADER2 = 0xe4 REQUEST_TRACK_NAME = 0xed # Reports REPORT_PLAYBACK = 0xf1 + REPORT_PLAYBACK2 = 0xf2 REPORT_LOADER = 0xf3 - REPORT_LOADER2 = 0xf4 + REPORT_LOADER2 = 0xf4 # Requested with REQUEST_LOADER REPORT_TOC = 0xf9 REPORT_TRACK_NAME = 0xfd diff --git a/decoders/avclan/pd.py b/decoders/avclan/pd.py index 57fc81be..86b57263 100644 --- a/decoders/avclan/pd.py +++ b/decoders/avclan/pd.py @@ -379,7 +379,7 @@ def pkt_from_cd_player(self): opcode = CDOpcodes(opcode) opcode_anno = opcode.name - if opcode == CDOpcodes.REPORT_PLAYBACK: + if opcode == CDOpcodes.REPORT_PLAYBACK or opcode == CDOpcodes.REPORT_PLAYBACK2: cd_state = self.data_bytes[2].b anno = str(CDStateCodes(cd_state))