From 66bbf8639ec076f0020e7364caeb7c9317982ef6 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Fri, 26 Jun 2026 20:10:35 -0700 Subject: [PATCH] Parameterise `Peripheral` by its `Device`s and add `CDChanger` as one --- .clang-tidy | 44 +- CMakeLists.txt | 5 +- src/avclan/avclan.hpp | 35 ++ src/avclan/avclan_defs.h | 1 + src/avclan/avclan_phy.h | 35 -- src/avclan/avclan_protocol.c | 368 ---------------- src/avclan/avclan_protocol.h | 39 -- src/avclan/avclandrv.h | 2 - src/avclan/bus.cc | 273 +++++++++++- src/avclan/bus.hpp | 45 +- src/avclan/cdchanger.c | 124 ------ src/avclan/cdchanger.cc | 407 ++++++++++++++++++ src/avclan/cdchanger.h | 85 ---- src/avclan/cdchanger.hpp | 93 ++++ src/avclan/device.hpp | 25 ++ src/avclan/peripheral.cc | 243 ----------- src/avclan/peripheral.hpp | 163 +++++-- src/avclan/statustimer.h | 4 +- src/avclan/target/avr-attiny3216/phy_avr.c | 4 +- .../target/avr-attiny3216/statustick_avr.c | 19 +- src/sniffer.cc | 55 +-- 21 files changed, 1047 insertions(+), 1022 deletions(-) create mode 100644 src/avclan/avclan.hpp delete mode 100644 src/avclan/avclan_protocol.c delete mode 100644 src/avclan/avclan_protocol.h delete mode 100644 src/avclan/cdchanger.c create mode 100644 src/avclan/cdchanger.cc delete mode 100644 src/avclan/cdchanger.h create mode 100644 src/avclan/cdchanger.hpp create mode 100644 src/avclan/device.hpp delete mode 100644 src/avclan/peripheral.cc diff --git a/.clang-tidy b/.clang-tidy index 1ae3f36..8ad0bc8 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,10 +1,32 @@ --- -Checks: "-*,clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-core.NullDereference,bugprone-*,misc-*,modernize-*,performance-*" +Checks: | + -*, + clang-diagnostic-*, + clang-analyzer-*, + -clang-analyzer-core.NullDereference, + bugprone-*, + misc-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -readability-braces-around-statements, WarningsAsErrors: "" -HeaderFilterRegex: "" -AnalyzeTemporaryDtors: false +ExcludeHeaderFilterRegex: 'out/build' +HeaderFilterRegex: '^src/.*' +SystemHeaders: false +# AnalyzeTemporaryDtors: false FormatStyle: none CheckOptions: + - key: readability-magic-numbers.IgnorePowersOf2IntegerValues + value: "true" + - key: readability-magic-numbers.IgnoredIntegerValues + value: "3;10;0x80;0xc0;0xFF;0x7f;0xf" + - key: readability-identifier-length.IgnoredVariableNames + value: "(in|to|id)" + - key: readability-identifier-length.IgnoredParameterNames + value: "(in|to)" - key: bugprone-argument-comment.CommentBoolLiterals value: "0" - key: bugprone-argument-comment.CommentCharacterLiterals @@ -31,8 +53,6 @@ CheckOptions: value: __builtin_expect - key: bugprone-dangling-handle.HandleClasses value: "std::basic_string_view;std::experimental::basic_string_view" - - key: bugprone-dynamic-static-initializers.HeaderFileExtensions - value: ";h;hh;hpp;hxx" - key: bugprone-easily-swappable-parameters.IgnoredParameterNames value: '"";iterator;Iterator;begin;Begin;end;End;first;First;last;Last;lhs;LHS;rhs;RHS' - key: bugprone-easily-swappable-parameters.IgnoredParameterTypeSuffixes @@ -105,10 +125,6 @@ CheckOptions: value: llvm - key: bugprone-suspicious-enum-usage.StrictMode value: "false" - - key: bugprone-suspicious-include.HeaderFileExtensions - value: ";h;hh;hpp;hxx" - - key: bugprone-suspicious-include.ImplementationFileExtensions - value: "c;cc;cpp;cxx" - key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens value: "5" - key: bugprone-suspicious-missing-comma.RatioThreshold @@ -151,19 +167,13 @@ CheckOptions: value: "false" - key: llvm-qualified-auto.AddConstToQualified value: "false" - - key: misc-definitions-in-headers.HeaderFileExtensions - value: ";h;hh;hpp;hxx" - - key: misc-definitions-in-headers.UseHeaderFileExtension - value: "true" - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic value: "false" - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables value: "false" - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries value: "true" - - key: misc-throw-by-value-catch-by-reference.MaxSize - value: "-1" - - key: misc-throw-by-value-catch-by-reference.WarnOnLargeObjects + - key: misc-throw-by-value-catch-by-reference.WarnOnLargeObject value: "false" - key: misc-uniqueptr-reset-release.IncludeStyle value: llvm @@ -243,8 +253,6 @@ CheckOptions: value: "true" - key: modernize-use-equals-delete.IgnoreMacros value: "true" - - key: modernize-use-nodiscard.ReplacementString - value: "[[nodiscard]]" - key: modernize-use-noexcept.ReplacementString value: "" - key: modernize-use-noexcept.UseNoexceptFalse diff --git a/CMakeLists.txt b/CMakeLists.txt index 01646bc..0d53e38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_compile_options( -Wall -Wswitch-enum -Werror + $<$:-fno-threadsafe-statics> $<$:-fno-exceptions> $<$:-fno-rtti> $<$:-fanalyzer>) @@ -32,9 +33,7 @@ set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build") # are contributed by its subdirectory. add_library(avclan STATIC src/avclan/avclan_frame.c - src/avclan/avclan_protocol.c - src/avclan/cdchanger.c - src/avclan/peripheral.cc + src/avclan/cdchanger.cc src/avclan/bus.cc ) diff --git a/src/avclan/avclan.hpp b/src/avclan/avclan.hpp new file mode 100644 index 0000000..ce43014 --- /dev/null +++ b/src/avclan/avclan.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace avclan::detail { +struct Error { + // Error enums are ordered such that a lower numeric value corresponds to + // more progress/success before an error occured, with 0 being no errors + enum class Read : uint8_t { + BAD_DATA_PARITY = 0x01, + BAD_LENGTH_RANGE, + BAD_LENGTH_PARITY, + BAD_PERIPHERAL_PARITY, + BAD_CONTROLLER_PARITY, + BAD_CONTROL_PARITY, + BAD_PARITY, // non-specific bad parity + STARTBIT_TOO_SHORT, + STARTBIT_TOO_LONG, + BAD_STARTBIT, + }; + + enum class Send : uint8_t { + NAK_DATA = 0x01, + NAK_MESSAGE_LENGTH, + NAK_CONTROL, + NAK_ADDRESS, + NAK, // non-specific NAK + BUSY, + MUTED, + }; +}; +} // namespace avclan::detail diff --git a/src/avclan/avclan_defs.h b/src/avclan/avclan_defs.h index 778e531..8bacaac 100644 --- a/src/avclan/avclan_defs.h +++ b/src/avclan/avclan_defs.h @@ -117,6 +117,7 @@ typedef struct print_struct { typedef struct AVCLAN_frame_struct { uint8_t reaction; + uint8_t owning_device; bool is_unicast; uint16_t controller_addr; // formerly "master" uint16_t peripheral_addr; // formerly "slave" diff --git a/src/avclan/avclan_phy.h b/src/avclan/avclan_phy.h index eccefcc..02ee202 100644 --- a/src/avclan/avclan_phy.h +++ b/src/avclan/avclan_phy.h @@ -61,38 +61,3 @@ void AVCLan_Measure(void); #ifdef __cplusplus } #endif - -#ifdef __cplusplus -template avclan_bit_t AVCLAN_sendbits(T bits); -template avclan_bit_t AVCLAN_readbits(T *bits); - -// Temporary specializations bridging to legacy C API -// Replace with proper (single?) template when phy has been ported -template - requires(N <= 8) -avclan_bit_t AVCLAN_sendbits(uint8_t bits) { - return AVCLAN_sendbitsi(&bits, N); -} -template - requires(N <= 16) -avclan_bit_t AVCLAN_sendbits(uint16_t bits) { - return AVCLAN_sendbitsl(&bits, N); -} -template <> inline avclan_bit_t AVCLAN_sendbits<8>(uint8_t byte) { - return AVCLAN_sendbyte(&byte); -} - -template - requires(N <= 8) -avclan_bit_t AVCLAN_readbits(uint8_t *bits) { - return static_cast(AVCLAN_readbitsi(bits, N)); -} -template - requires(N <= 16) -avclan_bit_t AVCLAN_readbits(uint16_t *bits) { - return static_cast(AVCLAN_readbitsl(bits, N)); -} -template <> inline avclan_bit_t AVCLAN_readbits<8>(uint8_t *byte) { - return static_cast(AVCLAN_readbyte(byte)); -} -#endif diff --git a/src/avclan/avclan_protocol.c b/src/avclan/avclan_protocol.c deleted file mode 100644 index 6d0f4e7..0000000 --- a/src/avclan/avclan_protocol.c +++ /dev/null @@ -1,368 +0,0 @@ -// copyright (C) 2006 Marcin Slonicki -// copyright (C) 2007 Louis Frigon -// Copyright (C) 2015 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include - -#include "avclan_phy.h" // AVCLAN_ismuted -#include "avclan_protocol.h" -#include "cdchanger.h" -#include "mediacontrol.h" -#include "statustimer.h" - -#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) - -static const uint8_t cdloading_resp[] = {dev_CD_CHANGER, - dev_STATUS, - Loading_Status_Report, - 0x00, - 0x01, - 0x00, - 0x01, - 0x00, - 0x01, - 0x02}; - -void AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { - out->reaction = r_Nothing; - - if (AVCLAN_ismuted() || in->length < 3) - return; - - // 0xFF placeholders are variant bytes filled by writing directly to - // out->data[N] after memcpy. - static const uint8_t lancheck_resp[] = {0x00, dev_COMM_CTRL, dev_LAN, 0xFF, - 0xFF}; - static const uint8_t function_change_resp[] = {0x00, dev_CD_CHANGER, - dev_COMM_v1, 0xFF, 0x01}; - - out->controller_addr = DEVICE_ADDR; - out->control = 0xF; - - const uint8_t *data = in->data; - const uint8_t b0 = *data++; - const uint8_t b1 = *data++; - const uint8_t b2 = *data++; - uint8_t b3 = 0; - if (in->length > 3) // the shortest known/valid messages are 3 bytes long - b3 = *data++; - - if (!in->is_unicast) { - // Broadcast: bytes are (from, to, action, [extra...]). - // peripheral_addr unchecked — always 0xFFF or 0x1FF in known traffic. - switch (PACK3(b0, b1, b2)) { - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Scan_Req): - out->length = sizeof(lancheck_resp); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); - out->data[3] = Lancheck_Scan_Resp; - out->data[4] = 0x01; - out->reaction = r_SendOnly; - break; - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Req): - out->length = sizeof(lancheck_resp); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); - out->data[3] = Lancheck_Resp; - out->data[4] = 0x00; - out->reaction = r_SendOnly; - break; - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_End_Req): - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->length = sizeof(lancheck_resp) - 1; - memcpy(out->data, lancheck_resp, out->length); - out->data[3] = Lancheck_End_Resp; - out->reaction = r_SendOnly; - break; - case PACK3(dev_COMM_v1, dev_COMM_CTRL, Current_Function): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, Current_Function): - if ((b3 == dev_CD_CHANGER) && !AVCLAN_isPlaying()) { - if (cd_status.mins > 99) - cd_status.mins = 0; - if (cd_status.secs > 99) - cd_status.secs = 0; - cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_STATUS); - out->reaction = r_StartPlaying; - } - break; - case PACK3(dev_COMM_v1, dev_COMM_CTRL, Ping_Req): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, Ping_Req): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - const uint8_t ping_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - Ping_Resp, 0xFF, b3}; - out->length = sizeof(ping_resp); - memcpy(out->data, ping_resp, sizeof(ping_resp)); - out->reaction = r_SendOnly; - break; - } - case PACK3(dev_COMM_v1, dev_COMM_CTRL, List_Functions_Req): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, List_Functions_Req): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - const uint8_t list_functions_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - List_Functions_Resp, - dev_CD_CHANGER}; - out->length = sizeof(list_functions_resp); - memcpy(out->data, list_functions_resp, sizeof(list_functions_resp)); - out->reaction = r_SendOnly; - break; - } - // case Restart_Lan: not handled - } - } else if (in->peripheral_addr == DEVICE_ADDR && b0 == 0x00) { - // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). - switch (PACK3(b1, b2, b3)) { - case PACK3(dev_COMM_v1, dev_CD_CHANGER, Enable_Function_Req): - [[fallthrough]]; - case PACK3(dev_COMM_v2, dev_CD_CHANGER, Enable_Function_Req): - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->length = sizeof(function_change_resp); - memcpy(out->data, function_change_resp, sizeof(function_change_resp)); - out->data[3] = Enable_Function_Resp; - cd_status.state = 0; - cd_status.flags2 = 0x80; - out->reaction = r_StatusReport; - break; - case PACK3(dev_COMM_v1, dev_CD_CHANGER, Disable_Function_Req): - [[fallthrough]]; - case PACK3(dev_COMM_v2, dev_CD_CHANGER, Disable_Function_Req): - // No change/response needed if we're already not playing - if (AVCLAN_isPlaying()) { - AVCLAN_stopPlaying(); - out->length = sizeof(function_change_resp); - memcpy(out->data, function_change_resp, sizeof(function_change_resp)); - out->data[3] = Disable_Function_Resp; - cd_status.state = 0; - cd_status.flags2 = 0x80; - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->reaction = r_StatusReport; - } - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Eject): { - // "Eject" label is multiply wrong; proper meaning unclear: - // - First observed on initial multiple presses of "CD" button, - // triggering (after {0x00, dev_CD_CHANGER, dev_COMM_v1, Insertion, - // 0x01} response) proper activation of mockingboard/cd-changer. - // - Subsequently observed when pressing (technically - // releasing?) the fast-forward button and rewind - if (cd_status.state | cd_SEEKING) { // FF/RW button released - cd_status.state &= ~cd_SEEKING; - } else { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - { - const uint8_t msg[] = {0x00, dev_CD_CHANGER, dev_CMD_SW, Insertion, - 0x01}; - out->length = sizeof(msg); - memcpy(out->data, msg, sizeof(msg)); - } - out->reaction = r_SendOnly; - } - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Initial_Report_Request): - [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Initial_Report_Request): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - - // No knowledge/understanding of field meaning/interpretation - const uint8_t cdinitreport_resp[] = { - 0x00, dev_CD_CHANGER, b1, Initial_Report_Response, 0x01, 0x31, - 0x10, 0x01, 0x01}; - out->length = sizeof(cdinitreport_resp); - memcpy(&out->data[1], cdinitreport_resp, sizeof(cdinitreport_resp)); - out->reaction = r_SendOnly; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Playback_Request): [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Playback_Request): - out->data[0] = 0x00; - out->data[1] = dev_CD_CHANGER; - out->data[2] = b1; - out->data[3] = Playback_Report; - out->length = sizeof(AVCLAN_CD_Status_t) + 4; - serializeCDStatus(&out->data[4]); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->reaction = r_SendOnly; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Loading_Request2): [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Loading_Request2): - out->data[0] = 0x00; - out->length = sizeof(cdloading_resp) + 1; - memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); - out->data[2] = b1; - out->data[3] = Loading_Response2; - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->reaction = r_SendOnly; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up): - cd_status.state = cd_SEEKING_TRACK; - if (cd_status.track < 98) - ++cd_status.track; - else - cd_status.track = 1; - cd_status.mins = 0xff; - cd_status.secs = 0x7f; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); - out->reaction = r_TrackChange; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Down): - cd_status.state = cd_SEEKING_TRACK; - // Track down returns to track beginning if in ~middle of song - if (cd_status.mins == 0 && cd_status.secs < 0x05) { - if (cd_status.track > 1) - --cd_status.track; - else - cd_status.track = 99; - } - cd_status.mins = 0xff; - cd_status.secs = 0x7f; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); - out->reaction = r_TrackChange; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Fast_Forward): { - cd_status.state |= cd_SEEKING; - cd_status.secs += 15; - if (cd_status.secs > 60) { - cd_status.secs -= 60; - ++cd_status.mins; - } - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now - out->reaction = r_SendOnly; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Rewind): { - cd_status.state |= cd_SEEKING; - if (cd_status.secs < 15) { - if (cd_status.mins > 0) { - uint8_t d = 15 - cd_status.secs; - cd_status.secs = 60 - d; - --cd_status.mins; - } else { - cd_status.mins = 0; - cd_status.secs = 0; - } - } else - cd_status.secs -= 15; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now - out->reaction = r_SendOnly; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Random): - cd_status.flags |= cd_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Random): - cd_status.flags &= ~cd_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Repeat): - cd_status.flags |= cd_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Repeat): - cd_status.flags &= ~cd_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Random): - cd_status.flags |= cd_DISK_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Random): - cd_status.flags &= ~cd_DISK_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Repeat): - cd_status.flags |= cd_DISK_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Repeat): - cd_status.flags &= ~cd_DISK_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - out->reaction = r_StatusReport; - break; - } - } -} - -#undef PACK3 - -void AVCLAN_statemachine(AVCLAN_frame_t *out) { - reaction_t r = out->reaction; - out->reaction = r_Nothing; - switch (r) { - case r_Ejection: { - const uint8_t play[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - Insertion, dev_CD_CHANGER, 0x01}; - out->length = sizeof(play); - memcpy(out->data, play, sizeof(play)); - } - out->reaction = r_Report_Load; - break; - case r_Report_Load: - out->is_unicast = false; - out->peripheral_addr = 0x1FF; - out->length = sizeof(cdloading_resp) + 1; - memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); - out->data[1] = dev_STATUS; - out->data[2] = Loading_Status_Report; - out->reaction = r_SendOnly; - break; - case r_TrackChange: - AVCLAN_setTime(0x00, 0x00); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now - [[fallthrough]]; - case r_NormalizeState: - AVCLAN_normalizeState(); - AVCLAN_generateStatus(out, true, dev_STATUS); - out->reaction = r_SendOnly; - break; - case r_StartPlaying: - AVCLAN_normalizeState(); - AVCLAN_generateStatus(out, true, dev_STATUS); - out->reaction = r_BeganPlaying; - break; - case r_BeganPlaying: - AVCLAN_startPlaying(); // only start PIT after normalizing state - out->reaction = r_Nothing; - break; - case r_StatusReport: - AVCLAN_generateStatus(out, true, dev_STATUS); - out->reaction = r_SendOnly; - break; - case r_SendOnly: [[fallthrough]]; - case r_Nothing: [[fallthrough]]; - default: out->reaction = r_Nothing; - } -} diff --git a/src/avclan/avclan_protocol.h b/src/avclan/avclan_protocol.h deleted file mode 100644 index aa7aa8c..0000000 --- a/src/avclan/avclan_protocol.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2026 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -// AVC-LAN message dispatcher and response state machine. Currently mixes -// generic peripheral-level handling with CD-changer device-specific handling; -// the device-specific cases will later migrate into cdchanger. - -#pragma once - -#include - -#include "avclan_defs.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/// Message state machine -// - r_Nothing (0x00) means don't send current message -// - All other instances mean send current message and imply the presence of -// follow-up messages within state machine -typedef enum : uint8_t { - r_Nothing = 0x00, - r_SendOnly, // No further follow-up needed (beyond sending current) - r_StatusReport = 0x02, // Needs follow-up status report - r_NormalizeState, // cd_status needs normalized and resent - r_StartPlaying, // ~equivalent to normalizeState, but cycles to BeganPlaying - r_BeganPlaying, - r_TrackChange, // Time needs reset - r_Ejection, - r_Report_Load, -} reaction_t; - -void AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out); -void AVCLAN_statemachine(AVCLAN_frame_t *out); - -#ifdef __cplusplus -} -#endif diff --git a/src/avclan/avclandrv.h b/src/avclan/avclandrv.h index dc4c137..53ac0b1 100644 --- a/src/avclan/avclandrv.h +++ b/src/avclan/avclandrv.h @@ -11,7 +11,5 @@ #include "avclan_defs.h" #include "avclan_frame.h" #include "avclan_phy.h" -#include "avclan_protocol.h" -#include "cdchanger.h" #include "mediacontrol.h" #include "statustimer.h" diff --git a/src/avclan/bus.cc b/src/avclan/bus.cc index 0b825d4..4c6d24e 100644 --- a/src/avclan/bus.cc +++ b/src/avclan/bus.cc @@ -3,28 +3,291 @@ // Copyright (C) 2015 Allen Hill // SPDX-License-Identifier: GPL-3.0-or-later +/* + AVC LAN Frame Format + │ Bits │ Description + ──────────────────────────────────────── + | 1 │ Start bit + | 1 │ Direct/broadcast + | 12 │ Controller address + | 1 │ Parity + | 12 │ Peripheral address + | 1 │ Parity + | 1 │ *Acknowledge* (read below) + | 4 │ Control + | 1 │ Parity + | 1 │ *Acknowledge* + | 8 │ Message length (n) + | 1 │ Parity + | 1 │ *Acknowledge* + ──────── + | 8 │ Data + | 1 │ Parity + | 1 │ *Acknowledge* + *repeat `n` times* + + No acknowledge bits are sent for broadcast frames. +*/ + #include "bus.hpp" +#include "avclan.hpp" #include "avclan_defs.h" +#include "avclan_frame.h" #include "avclan_phy.h" // bridge until phy has been ported +#include "com232.h" + +namespace { +constexpr int ADDR_WIDTH = 12; +} namespace avclan { void Bus::init() { AVCLAN_busInit(); }; void Bus::mute(bool mute) { AVCLAN_muteDevice(mute); }; bool Bus::is_muted() const { return AVCLAN_ismuted(); }; + +auto Bus::read(uint16_t address, AVCLAN_frame_t *in, log_t print) + -> Error::Read { + struct errtype { + Error::Read errno; + union { + uint8_t val; // BAD_LENGTH_RANGE: the out-of-range length value + struct { + uint8_t parity; // received (bad) parity bit + uint16_t read_val; + }; + }; + } err = {}; + + using enum detail::Error::Read; + + { // bound handle lifetime + auto handle = get(); + + bool shouldACK = false; + uint8_t tmp = 0; + + err.errno = Error::Read{static_cast(handle.readstartbit())}; + if (static_cast(err.errno)) + goto handle_err; + + handle.read<1>(&tmp, false); + in->is_unicast = tmp; + + if (auto rerr = handle.read(&in->controller_addr, false); + rerr == BAD_PARITY) { + err.errno = BAD_CONTROLLER_PARITY; + if (print.verbose) { + err.read_val = in->controller_addr; + } + goto handle_err; + } + + if (auto rerr = handle.read( + &in->peripheral_addr, + [&]() { return !is_muted() && (in->peripheral_addr == address); }); + rerr == BAD_PARITY) { + err.errno = BAD_PERIPHERAL_PARITY; + if (print.verbose) { + err.read_val = in->peripheral_addr; + } + goto handle_err; + } + + shouldACK = !is_muted() && (in->peripheral_addr == address); + + if (auto rerr = handle.read<4>(&in->control, shouldACK); + rerr == BAD_PARITY) { + err.errno = BAD_CONTROL_PARITY; + if (print.verbose) { + err.read_val = in->control; + } + goto handle_err; + } + + if (auto rerr = handle.read<8>(&in->length, shouldACK); + rerr == BAD_PARITY) { + err.errno = BAD_LENGTH_PARITY; + if (print.verbose) { + err.read_val = in->length; + } + goto handle_err; + } + + if (in->length == 0 || in->length > MAXMSGLEN) { + err.errno = BAD_LENGTH_RANGE; + err.val = in->length; + goto handle_err; + } + + for (uint8_t i = 0; i < in->length; i++) { + if (auto rerr = handle.read<8>(&in->data[i], shouldACK); + rerr == BAD_PARITY) { + err.errno = BAD_DATA_PARITY; + if (print.verbose) { + err.read_val = in->data[i]; + } + goto handle_err; + } + } + } // destroy handle + + if (false) { // NOLINT(readability-simplify-boolean-expr) + handle_err:; + RS232_Print("ERR(read): "); + switch (err.errno) { + case BAD_STARTBIT: RS232_Print("bad start bit (other)"); break; + case STARTBIT_TOO_SHORT: RS232_Print("bad start bit (short)"); break; + case STARTBIT_TOO_LONG: RS232_Print("bad start bit (long)"); break; + case BAD_CONTROLLER_PARITY: + RS232_Print("reading controller addr."); + goto VERBOSE; + case BAD_PERIPHERAL_PARITY: + RS232_Print("reading peripheral addr."); + goto VERBOSE; + case BAD_CONTROL_PARITY: RS232_Print("reading control"); goto VERBOSE; + case BAD_LENGTH_PARITY: RS232_Print("reading length"); goto VERBOSE; + case BAD_LENGTH_RANGE: + RS232_Print("bad length 0x"); + RS232_PrintHex4(err.val); + break; + case BAD_DATA_PARITY: + RS232_Print("reading data"); + goto VERBOSE; + VERBOSE: + if (print.verbose) { + RS232_Print("; read 0x"); + RS232_PrintHex(err.read_val); + RS232_Print(" and got bad parity "); + RS232_PrintHex4(err.parity); + } + case BAD_PARITY: __builtin_unreachable(); + } + RS232_Print("\n"); + } + + // Only print if some data has been correctly received + if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { + if (err.errno > BAD_DATA_PARITY) + in->length = 0; + AVCLAN_printframe(in, print.binary); + } + + return err.errno; +} + +auto Bus::send(const AVCLAN_frame_t *out, log_t print) -> Error::Send { + struct errtype { + // Error enum is ordered such that a lower numeric value corresponds to + // more success + Error::Send errno; + uint8_t val; + } err = {}; + + using enum detail::Error::Send; + + if (is_muted()) { + err.errno = MUTED; + goto handle_err; + } + + { // bound handle lifetime + auto handle = get(); + + if (!handle.sendstartbit()) { + // Some other device is already driving the bus + err.errno = BUSY; + goto handle_err; + } + + handle.send<1>(static_cast(out->is_unicast), false); + + handle.send(out->controller_addr, false); + + if (auto serr = + handle.send(out->controller_addr, out->is_unicast); + serr == NAK) { + err.errno = NAK_ADDRESS; + goto handle_err; + } + + if (auto serr = handle.send<4>(out->control, out->is_unicast); + serr == NAK) { + err.errno = NAK_CONTROL; + goto handle_err; + } + + if (auto serr = handle.send<8>(out->length, out->is_unicast); serr == NAK) { + err.errno = NAK_MESSAGE_LENGTH; + goto handle_err; + } + + for (uint8_t i = 0; i < out->length; i++) { + // Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem + // necessary (i.e. This deviates from the previous broadcast specific + // function that sent an extra `1` bit after each byte/parity) + // Explanation for why audio-group broadcast state report isn't working? + if (auto serr = handle.send<8>(out->data[i], out->is_unicast); + serr == NAK) { + err.errno = NAK_DATA; + err.val = i; + goto handle_err; + } + } + } // destroy handle + + // back to read mode + if (false) { // NOLINT(readability-simplify-boolean-expr) + handle_err:; + RS232_Print("Error"); + switch (err.errno) { + case MUTED: RS232_Print(": Device muted"); break; + case BUSY: RS232_Print(": Busy bus"); break; + case NAK_ADDRESS: + case NAK_CONTROL: + case NAK_MESSAGE_LENGTH: + case NAK_DATA: + case NAK: + RS232_Print(" NAK: "); + switch (err.errno) { + case NAK_ADDRESS: RS232_Print("address"); break; + case NAK_CONTROL: RS232_Print("Control"); break; + case NAK_MESSAGE_LENGTH: RS232_Print("Message length"); break; + case NAK_DATA: + RS232_Print(" data["); + RS232_PrintDec(err.val); + RS232_Print("]"); + break; + case NAK: + case MUTED: + case BUSY: __builtin_unreachable(); + } + break; + } + RS232_Print("\n"); + } + + if (print.print) + AVCLAN_printframe(out, print.binary); + + return err.errno; +} + Bus::Handle Bus::get() { return {}; }; bool Bus::Handle::sendstartbit() { return AVCLAN_sendstartbit(); }; -Bus::Error::Read Bus::Handle::readstartbit() { +auto Bus::Handle::readstartbit() -> Read { using enum Error::Read; auto err = AVCLAN_readstartbit(); if (err == rSTARTBIT_TOO_LONG) return STARTBIT_TOO_LONG; - else if (err == rLATCHED_COMPARATOR) + + if (err == rLATCHED_COMPARATOR) return BAD_STARTBIT; - else if (err == rSTARTBIT_TOO_SHORT) + + if (err == rSTARTBIT_TOO_SHORT) return STARTBIT_TOO_SHORT; - else - return Read{0}; + + return Read{0}; }; void Bus::Handle::send_ACK() { AVCLAN_sendbit_ACK(); }; uint8_t Bus::Handle::read_ACK() { return AVCLAN_readbit_ACK(); }; diff --git a/src/avclan/bus.hpp b/src/avclan/bus.hpp index 9835c94..c2b724c 100644 --- a/src/avclan/bus.hpp +++ b/src/avclan/bus.hpp @@ -49,31 +49,24 @@ #include +#include "avclan.hpp" #include "avclan_defs.h" #include "avclan_phy.h" // bridge until phy has been ported namespace avclan { class Bus { public: - struct Error { - enum class Read : uint8_t { - BAD_PARITY = 0x01, - STARTBIT_TOO_SHORT = 0x80, // Start *well* above Peripher::Error::Read - // (which ~inherits these values) - STARTBIT_TOO_LONG, - BAD_STARTBIT, - }; - - enum class Send : uint8_t { - NAK = 0x01, - }; - }; class Handle; + using Error = detail::Error; void init(); void mute(bool mute); bool is_muted() const; - Handle get(); + + Error::Read read(uint16_t address, AVCLAN_frame_t *in, log_t print); + Error::Send send(const AVCLAN_frame_t *out, log_t print); + + static Handle get(); }; class Bus::Handle { @@ -84,6 +77,7 @@ public: ~Handle() { AVCLAN_startEvent(); } Handle(const Handle &) = delete; Handle(Handle &&) = delete; + using Error = detail::Error; bool sendstartbit(); Error::Read readstartbit(); @@ -108,7 +102,8 @@ public: readbits<1>(&read_parity); if (calc_parity != read_parity) { return Read::BAD_PARITY; - } else if (ack()) { + } + if (ack()) { send_ACK(); } else readbits<1>(&read_parity); @@ -133,8 +128,8 @@ private: bit_start = 0x10 }; - void send_ACK(); - uint8_t read_ACK(); + static void send_ACK(); + static uint8_t read_ACK(); template avclan_bit_t sendbits(T bits); template avclan_bit_t readbits(T *bits); @@ -163,16 +158,16 @@ private: }; }; -template <> inline avclan_bit_t Bus::Handle::sendbits<8>(uint8_t byte) { - return AVCLAN_sendbyte(&byte); +template <> inline avclan_bit_t Bus::Handle::sendbits<8>(uint8_t bits) { + return AVCLAN_sendbyte(&bits); }; -template <> inline avclan_bit_t Bus::Handle::sendbits<1>(uint8_t byte) { - const avclan_bit_t b{static_cast(byte & 1u)}; - AVCLAN_sendbit(b); - return b; +template <> inline avclan_bit_t Bus::Handle::sendbits<1>(uint8_t bits) { + const avclan_bit_t bit{static_cast(bits & 1U)}; + AVCLAN_sendbit(bit); + return bit; }; -template <> inline avclan_bit_t Bus::Handle::readbits<8>(uint8_t *byte) { - return static_cast(AVCLAN_readbyte(byte)); +template <> inline avclan_bit_t Bus::Handle::readbits<8>(uint8_t *bits) { + return static_cast(AVCLAN_readbyte(bits)); }; } // namespace avclan diff --git a/src/avclan/cdchanger.c b/src/avclan/cdchanger.c deleted file mode 100644 index 1585999..0000000 --- a/src/avclan/cdchanger.c +++ /dev/null @@ -1,124 +0,0 @@ -// copyright (C) 2006 Marcin Slonicki -// copyright (C) 2007 Louis Frigon -// Copyright (C) 2015 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include - -#include "avclan_phy.h" -#include "cdchanger.h" -#include "mediacontrol.h" -#include "statustimer.h" - -AVCLAN_CD_Status_t cd_status; - -static cd_modes CD_Mode; - -// Sets CD_mode to play and resets timer count (so that the next interrupt is in -// 1 sec) -void AVCLAN_startPlaying() { - static bool havePlayed = false; - if (havePlayed) - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); - havePlayed |= true; - CD_Mode = stPlay; - statustimer_reset(); -} - -// Sets CD_mode to play and resets timer count (so that the next interrupt is in -// 1 sec) -void AVCLAN_stopPlaying() { - statustimer_disable(); - CD_Mode = stStop; - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); -} - -/* Pack a 0–99 count into 2-digit BCD. Values >99 (sentinels such as 0xFF / - 0x7F meaning "no time") pass through unchanged so they survive the wire - round-trip. */ -static uint8_t toBCD(uint8_t x) { - if (x > 99) - return x; - return (uint8_t)(((x / 10) << 4) | (x % 10)); -} - -// Serialize cd_status into the wire format. The struct layout mirrors the wire -// format byte-for-byte, except for track/mins/secs, which need converted from -// decimal to BCD -void serializeCDStatus(uint8_t *dst) { - memcpy(dst, &cd_status, sizeof(cd_status)); - dst[3] = toBCD(cd_status.track); - dst[4] = toBCD(cd_status.mins); - dst[5] = toBCD(cd_status.secs); -} - -bool AVCLAN_isPlaying() { return (CD_Mode == stPlay); } - -void AVCLAN_incrementTime() { - // Sentinel values (>99) mean "no time"; leave them alone until setTime() - // replaces them with a real count. - if (cd_status.secs > 99) - return; - if (cd_status.secs == 59) { - cd_status.secs = 0; - if (cd_status.mins == 99) - cd_status.mins = 0; - else - cd_status.mins++; - } else - cd_status.secs++; -} - -void AVCLAN_setTime(uint8_t mins, uint8_t secs) { - cd_status.mins = mins; - cd_status.secs = secs; -} - -// Used for changed status messages -void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, - devices to) { - status->is_unicast = is_unicast; - status->controller_addr = DEVICE_ADDR; - status->peripheral_addr = (is_unicast) ? HU_ADDR : 0x1FF; - status->control = 0xF; - status->length = sizeof(AVCLAN_CD_Status_t) + ((is_unicast) ? 4 : 3); - - uint8_t *data = status->data; - if (is_unicast) - *data++ = 0x00; - *data++ = dev_CD_CHANGER; - *data++ = to; - *data++ = Status_Report; - serializeCDStatus(data); -} - -void AVCLAN_normalizeState() { - // if (cd_status.state != cd_PLAYBACK) { - if (cd_status.mins > 99) - cd_status.mins = 0; - if (cd_status.secs > 99) - cd_status.secs = 0; - cd_status.state = cd_PLAYBACK; - cd_status.flags &= (uint8_t)~(cd_DISK_SCAN | cd_SCAN); - cd_status.flags2 = 0x80; - // } -} - -void AVCLAN_init() { - AVCLAN_busInit(); - mediacontrol_init(); - statustimer_init(); - - cd_status.cds = cd_CD1; - cd_status.disc = 1; - cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; - cd_status.flags = 0; - cd_status.flags2 = 0xC0; - - cd_status.track = 1; - cd_status.mins = 0xFF; - cd_status.secs = 0x7F; - - CD_Mode = stStop; -} diff --git a/src/avclan/cdchanger.cc b/src/avclan/cdchanger.cc new file mode 100644 index 0000000..67d0a67 --- /dev/null +++ b/src/avclan/cdchanger.cc @@ -0,0 +1,407 @@ +// copyright (C) 2006 Marcin Slonicki +// copyright (C) 2007 Louis Frigon +// Copyright (C) 2015 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "avclan_defs.h" +#include "cdchanger.hpp" +#include "mediacontrol.h" +#include "statustimer.h" + +namespace { + +constexpr uint8_t cdloading_resp[] = {dev_CD_CHANGER, + dev_STATUS, + Loading_Status_Report, + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x01, + 0x02}; + +constexpr int WIRE_SIZE = 8; +constexpr int TIME_SKIP = 15; +constexpr int TWODIGIT_MAX = 99; + +/* Pack a 0–TWODIGIT_MAX count into 2-digit BCD. Values >TWODIGIT_MAX (sentinels + such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive + the wire round-trip. */ +constexpr uint8_t toBCD(uint8_t val) { + if (val > TWODIGIT_MAX) + return val; + return (uint8_t)(((val / 10) << 4) | (val % 10)); +} + +extern "C" void incrementTime_callback(void *self) { + static_cast(self)->incrementTime(); +} + +extern "C" bool isPlaying_callback(void *self) { + return static_cast(self)->isPlaying(); +} +} // namespace + +namespace avclan { + +void CDChanger::init() { + mediacontrol_init(); + statustimer_init(this, &incrementTime_callback, &isPlaying_callback); +} + +void CDChanger::handle(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { + const uint8_t *data = &in->data[1]; + const auto from = static_cast(*data++); + /* const auto to = */ data++; + const auto action = *data++; + + static const uint8_t function_change_resp[] = {0x00, dev_CD_CHANGER, from, + 0xFF, 0x01}; + + // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). + switch (action) { + case Enable_Function_Req: + out->is_unicast = true; + out->length = sizeof(function_change_resp); + memcpy(out->data, function_change_resp, sizeof(function_change_resp)); + out->data[3] = Enable_Function_Resp; + state = 0; + flags2 = 0x80; + out->reaction = r_StatusReport; + break; + case Disable_Function_Req: + // No change/response needed if we're already not playing + if (isPlaying()) { + stopPlaying(); + out->length = sizeof(function_change_resp); + memcpy(out->data, function_change_resp, sizeof(function_change_resp)); + out->data[3] = Disable_Function_Resp; + state = 0; + flags2 = 0x80; + out->is_unicast = true; + out->reaction = r_StatusReport; + } + break; + case Eject: { + // "Eject" label is multiply wrong; proper meaning unclear: + // - First observed on initial multiple presses of "CD" button, + // triggering (after {0x00, dev_CD_CHANGER, dev_COMM_v1, Insertion, + // 0x01} response) proper activation of mockingboard/cd-changer. + // - Subsequently observed when pressing (technically + // releasing?) the fast-forward button and rewind + if (static_cast(state & SEEKING)) { // FF/RW button released + state &= ~SEEKING; + } else { + out->is_unicast = true; + { + const uint8_t msg[] = {0x00, dev_CD_CHANGER, dev_CMD_SW, Insertion, + 0x01}; + out->length = sizeof(msg); + memcpy(out->data, msg, sizeof(msg)); + } + out->reaction = r_SendOnly; + } + break; + } + case Initial_Report_Request: { + out->is_unicast = true; + // No knowledge/understanding of field meaning/interpretation + const uint8_t cdinitreport_resp[] = { + 0x00, dev_CD_CHANGER, from, Initial_Report_Response, 0x01, 0x31, + 0x10, 0x01, 0x01}; + out->length = sizeof(cdinitreport_resp); + memcpy(&out->data[1], cdinitreport_resp, sizeof(cdinitreport_resp)); + out->reaction = r_SendOnly; + break; + } + case Playback_Request: + out->data[0] = 0x00; + out->data[1] = dev_CD_CHANGER; + out->data[2] = from; + out->data[3] = Playback_Report; + out->length = WIRE_SIZE + 4; + serialize(&out->data[4]); + out->is_unicast = true; + out->reaction = r_SendOnly; + break; + case Loading_Request2: + out->data[0] = 0x00; + out->length = sizeof(cdloading_resp) + 1; + memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); + out->data[2] = from; + out->data[3] = Loading_Response2; + out->is_unicast = true; + out->reaction = r_SendOnly; + break; + case Track_Seek_Up: + state = SEEKING_TRACK; + if (track < 98) + ++track; + else + track = 1; + mins = 0xff; + secs = 0x7f; + flags2 = 0xc0; + generateStatus(out, true, dev_CMD_SW); + AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); + out->reaction = r_TrackChange; + break; + case Track_Seek_Down: + state = SEEKING_TRACK; + // Track down returns to track beginning if in ~middle of song + if (mins == 0 && secs < 5) { + if (track > 1) + --track; + else + track = TWODIGIT_MAX; + } + mins = 0xff; + secs = 0x7f; + flags2 = 0xc0; + generateStatus(out, true, dev_CMD_SW); + AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); + out->reaction = r_TrackChange; + break; + case Track_Fast_Forward: { + state |= SEEKING; + secs += TIME_SKIP; + if (secs > 60) { + secs -= 60; + ++mins; + } + generateStatus(out, true, dev_CMD_SW); + AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick + // is ~1 sec from now + out->reaction = r_SendOnly; + break; + } + case Track_Rewind: { + state |= SEEKING; + if (secs < TIME_SKIP) { + if (mins > 0) { + const uint8_t dif = TIME_SKIP - secs; + secs = 60 - dif; + --mins; + } else { + mins = 0; + secs = 0; + } + } else + secs -= TIME_SKIP; + generateStatus(out, true, dev_CMD_SW); + AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick + // is ~1 sec from now + out->reaction = r_SendOnly; + break; + } + case CD_Enable_Random: + flags |= RANDOM; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Disable_Random: + flags &= ~RANDOM; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Enable_Repeat: + flags |= REPEAT; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Disable_Repeat: + flags &= ~REPEAT; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Enable_Disk_Random: + flags |= DISK_RANDOM; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Disable_Disk_Random: + flags &= ~DISK_RANDOM; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Enable_Disk_Repeat: + flags |= DISK_REPEAT; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + case CD_Disable_Disk_Repeat: + flags &= ~DISK_REPEAT; + generateStatus(out, true, dev_CMD_SW); + out->reaction = r_StatusReport; + break; + default: break; + } +} + +void CDChanger::react(AVCLAN_frame_t *out, detail::Error::Send err) { + auto resp = static_cast(out->reaction); + out->reaction = r_Nothing; + switch (resp) { + case r_StateReport: + if (err == detail::Error::Send::NAK_ADDRESS && + ++failedStatusReports > 1) { + failedStatusReports = 0; + stopPlaying(); // Disable periodic updates if e.g. no-one's + // listening (car was turned off?) + } + break; + case r_Ejection: { + const uint8_t play[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, + Insertion, dev_CD_CHANGER, 0x01}; + out->length = sizeof(play); + memcpy(out->data, play, sizeof(play)); + } + out->reaction = r_Report_Load; + break; + case r_Report_Load: + out->is_unicast = false; + out->peripheral_addr = 0x1FF; + out->length = sizeof(cdloading_resp) + 1; + memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); + out->data[1] = dev_STATUS; + out->data[2] = Loading_Status_Report; + out->reaction = r_SendOnly; + break; + case r_TrackChange: + setTime(0x00, 0x00); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now + [[fallthrough]]; + case r_NormalizeState: + normalizeState(); + generateStatus(out, true, dev_STATUS); + out->reaction = r_SendOnly; + break; + case r_StartPlaying: + normalizeState(); + generateStatus(out, true, dev_STATUS); + out->reaction = r_BeganPlaying; + break; + case r_BeganPlaying: + startPlaying(); // only start PIT after normalizing state + out->reaction = r_Nothing; + break; + case r_StatusReport: + generateStatus(out, true, dev_STATUS); + out->reaction = r_SendOnly; + break; + case r_SendOnly: [[fallthrough]]; + case r_Nothing: [[fallthrough]]; + default: out->reaction = r_Nothing; + } +} + +void CDChanger::enable(AVCLAN_frame_t *out) { + if (!isPlaying()) { + if (mins > TWODIGIT_MAX) + mins = 0; + if (secs > TWODIGIT_MAX) + secs = 0; + state = SEEKING | SEEKING_TRACK; + flags2 = 0xc0; + generateStatus(out, true, dev_STATUS); + out->reaction = r_StartPlaying; + } +} + +bool CDChanger::pending() { return statustimer_tickPending(); } +void CDChanger::resolvepending() { statustimer_clearTick(); } + +void CDChanger::emit(AVCLAN_frame_t *out) { + generateStatus(out, true, dev_STATUS); + out->reaction = r_StateReport; +} + +bool CDChanger::isPlaying() const { return playing; } + +// Sets CD_mode to play and resets timer count (so that the next interrupt is in +// 1 sec) +void CDChanger::startPlaying() { + static bool havePlayed = false; + if (havePlayed) + AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); + havePlayed |= true; + playing = true; + statustimer_reset(); +} + +void CDChanger::stopPlaying() { + statustimer_disable(); + playing = false; + AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); +} + +// Serialize cd_status into the wire format. The struct layout mirrors the wire +// format byte-for-byte, except for track/mins/secs, which need converted from +// decimal to BCD +void CDChanger::serialize(uint8_t *dst) const { + *dst++ = cds; + *dst++ = state; + *dst++ = disc; + *dst++ = toBCD(track); + *dst++ = toBCD(mins); + *dst++ = toBCD(secs); + *dst++ = flags; + *dst++ = flags2; +} + +void CDChanger::setTime(uint8_t min, uint8_t sec) { + mins = min; + secs = sec; +} + +void CDChanger::incrementTime() { + // Sentinel values (>TWODIGIT_MAX) mean "no time"; leave them alone until + // setTime() replaces them with a real count. + if (secs > TWODIGIT_MAX) + return; + if (secs == 59) { + secs = 0; + if (mins == TWODIGIT_MAX) + mins = 0; + else + mins++; + } else + secs++; +} + +// Used for changed status messages +void CDChanger::generateStatus(AVCLAN_frame_t *status, bool is_unicast, + devices to) const { + status->is_unicast = is_unicast; + if (!is_unicast) + status->peripheral_addr = 0x1FF; + status->control = 0xF; + status->length = WIRE_SIZE + ((is_unicast) ? 4 : 3); + + uint8_t *data = status->data; + if (is_unicast) + *data++ = 0x00; + *data++ = dev_CD_CHANGER; + *data++ = to; + *data++ = Status_Report; + serialize(data); +} + +void CDChanger::normalizeState() { + if (mins > TWODIGIT_MAX) + mins = 0; + if (secs > TWODIGIT_MAX) + secs = 0; + state = PLAYBACK; + flags &= (uint8_t)~(DISK_SCAN | SCAN); + flags2 = 0x80; +} + +} // namespace avclan diff --git a/src/avclan/cdchanger.h b/src/avclan/cdchanger.h deleted file mode 100644 index ee722d6..0000000 --- a/src/avclan/cdchanger.h +++ /dev/null @@ -1,85 +0,0 @@ -// copyright (C) 2006 Marcin Slonicki -// copyright (C) 2007 Louis Frigon -// Copyright (C) 2015 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -// Emulated CD-changer device (AVC-LAN device 0x63): playback state, BCD time, -// status-frame generation, and the play/stop mode FSM. This is pure-ish device -// logic; it commands the mediacontrol and statustimer drivers rather than -// touching their hardware directly. - -#pragma once - -#include - -#include "avclan_defs.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum : uint8_t { - cd_OPEN = 0x01, - cd_ERR1 = 0x02, - cd_SEEKING = 0x08, - cd_PLAYBACK = 0x10, - cd_SEEKING_TRACK = 0x20, - cd_LOADING = 0x80, -} cd_state; - -typedef enum : uint8_t { - cd_CD1 = 1 << 0, - cd_CD2 = 1 << 1, - cd_CD3 = 1 << 2, - cd_CD4 = 1 << 3, - cd_CD5 = 1 << 4, - cd_CD6 = 1 << 5, -} cd_present_t; - -typedef enum : uint8_t { - cd_DISK_RANDOM = 1 << 1, - cd_RANDOM = 1 << 2, - cd_DISK_REPEAT = 1 << 3, - cd_REPEAT = 1 << 4, - cd_DISK_SCAN = 1 << 5, - cd_SCAN = 1 << 6, -} cd_flag_t; - -typedef struct AVCLAN_CD_Status { - uint8_t cds; - uint8_t state; - uint8_t disc; - uint8_t track; // Decimal storage; serialize to BCD - uint8_t mins; // Decimal storage; serialize to BCD - uint8_t secs; // Decimal storage; serialize to BCD - uint8_t flags; - uint8_t flags2; -} AVCLAN_CD_Status_t; - -typedef enum : uint8_t { stStop = 0, stPlay = 1 } cd_modes; - -// Mutable device state. Exposed for the protocol dispatcher, which currently -// mutates these fields directly. (Will be re-encapsulated when the device- -// specific message handling migrates here from avclan_protocol.) -extern AVCLAN_CD_Status_t cd_status; - -// Full device-stack bring-up (calls AVCLAN_phyInit / mediacontrol_init / -// statustimer_init, then initialises cd_status). -void AVCLAN_init(); - -bool AVCLAN_isPlaying(); -void AVCLAN_startPlaying(); -void AVCLAN_stopPlaying(); -void AVCLAN_incrementTime(); -void AVCLAN_setTime(uint8_t mins, uint8_t secs); -void AVCLAN_normalizeState(); - -// Serialize cd_status into the wire format (track/mins/secs as BCD). -void serializeCDStatus(uint8_t *dst); - -AVCLAN_frame_t *AVCLAN_getStatusFrame(); -void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, devices to); - -#ifdef __cplusplus -} -#endif diff --git a/src/avclan/cdchanger.hpp b/src/avclan/cdchanger.hpp new file mode 100644 index 0000000..659f419 --- /dev/null +++ b/src/avclan/cdchanger.hpp @@ -0,0 +1,93 @@ +// copyright (C) 2006 Marcin Slonicki +// copyright (C) 2007 Louis Frigon +// Copyright (C) 2015 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "avclan.hpp" +#include "avclan_defs.h" + +namespace avclan { + +class CDChanger { +public: + enum State : uint8_t { + OPEN = 0x01, + ERR1 = 0x02, + SEEKING = 0x08, + PLAYBACK = 0x10, + SEEKING_TRACK = 0x20, + LOADING = 0x80, + }; + enum CD : uint8_t { + CD1 = 1 << 0, + CD2 = 1 << 1, + CD3 = 1 << 2, + CD4 = 1 << 3, + CD5 = 1 << 4, + CD6 = 1 << 5, + }; + enum Flags : uint8_t { + DISK_RANDOM = 1 << 1, + RANDOM = 1 << 2, + DISK_REPEAT = 1 << 3, + REPEAT = 1 << 4, + DISK_SCAN = 1 << 5, + SCAN = 1 << 6, + }; + + /// Message state machine + // - r_Nothing (0x00) means don't send current message + // - All other instances mean send current message and imply the presence of + // follow-up messages within state machine + enum reaction_t : uint8_t { + r_Nothing = 0x00, + r_SendOnly, // No further follow-up needed (beyond sending current) + r_StatusReport, // Needs follow-up status report + r_NormalizeState, // cd_status needs normalized and resent + r_StartPlaying, // ~equivalent to normalizeState, but cycles to BeganPlaying + r_BeganPlaying, + r_TrackChange, // Time needs reset + r_Ejection, + r_Report_Load, + r_StateReport, // *IS* a status report (follow-up or unprompted) + }; + + static constexpr uint8_t id = dev_CD_CHANGER; + void init(); + + void handle(const AVCLAN_frame_t *in, AVCLAN_frame_t *out); + void react(AVCLAN_frame_t *out, detail::Error::Send err); + void enable(AVCLAN_frame_t *out); + void disable(AVCLAN_frame_t *out); + static bool pending(); + static void resolvepending(); + void emit(AVCLAN_frame_t *out); + void incrementTime(); + bool isPlaying() const; + +private: + void startPlaying(); + void stopPlaying(); + void serialize(uint8_t *dst) const; + void setTime(uint8_t mins, uint8_t secs); + void generateStatus(AVCLAN_frame_t *status, bool is_unicast, + devices to) const; + void normalizeState(); + + bool playing = false; + int failedStatusReports = 0; + uint8_t cds = CD1; + uint8_t state = SEEKING | SEEKING_TRACK; + uint8_t disc = 1; + uint8_t track = 1; // Decimal storage; serialize to BCD + uint8_t mins = 0xFF; // Decimal storage; serialize to BCD + uint8_t secs = 0x7F; // Decimal storage; serialize to BCD + uint8_t flags = 0; + uint8_t flags2 = 0xC0; +}; + +} // namespace avclan diff --git a/src/avclan/device.hpp b/src/avclan/device.hpp new file mode 100644 index 0000000..17c7609 --- /dev/null +++ b/src/avclan/device.hpp @@ -0,0 +1,25 @@ +// copyright (C) 2026 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "avclan.hpp" +#include "avclan_defs.h" + +#include +#include + +namespace avclan { +template +concept Device = requires { std::integral_constant{}; } && + requires(T dev, const AVCLAN_frame_t *in, AVCLAN_frame_t *out, + detail::Error::Send err) { + dev.init(); + dev.handle(in, out); + dev.enable(out); + dev.react(out, err); + { dev.pending() } -> std::convertible_to; + dev.resolvepending(); + dev.emit(out); + }; +} // namespace avclan diff --git a/src/avclan/peripheral.cc b/src/avclan/peripheral.cc deleted file mode 100644 index cc9b63e..0000000 --- a/src/avclan/peripheral.cc +++ /dev/null @@ -1,243 +0,0 @@ -// copyright (C) 2006 Marcin Slonicki -// copyright (C) 2007 Louis Frigon -// Copyright (C) 2015 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "peripheral.hpp" -#include "avclan_defs.h" -#include "avclan_frame.h" -#include "bus.hpp" -#include "com232.h" // error logging - -#include - -namespace avclan { - -Peripheral::Error::Read Peripheral::read(AVCLAN_frame_t *in, log_t print) { - struct errtype { - Error::Read errno; - union { - uint8_t val; // BAD_LENGTH_RANGE: the out-of-range length value - struct { - uint8_t parity; // received (bad) parity bit - uint16_t read_val; - }; - }; - } err = {}; - - using enum Error::Read; - auto BAD_PARITY = Bus::Error::Read::BAD_PARITY; - - { // bound handle lifetime - auto handle = bus.get(); - - bool shouldACK = false; - uint8_t tmp = 0; - - err.errno = Error::Read{static_cast(handle.readstartbit())}; - if (static_cast(err.errno)) - goto handle_err; - - handle.read<1>(&tmp, false); - in->is_unicast = tmp; - - if (auto rerr = handle.read<12>(&in->controller_addr, false); - rerr == BAD_PARITY) { - err.errno = BAD_CONTROLLER_PARITY; - if (print.verbose) { - err.read_val = in->controller_addr; - } - goto handle_err; - } - - if (auto rerr = handle.read<12>(&in->peripheral_addr, - [&]() { - return !bus.is_muted() && - (in->peripheral_addr == address); - }); - rerr == BAD_PARITY) { - err.errno = BAD_PERIPHERAL_PARITY; - if (print.verbose) { - err.read_val = in->peripheral_addr; - } - goto handle_err; - } - - shouldACK = !bus.is_muted() && (in->peripheral_addr == address); - - if (auto rerr = handle.read<4>(&in->control, shouldACK); - rerr == BAD_PARITY) { - err.errno = BAD_CONTROL_PARITY; - if (print.verbose) { - err.read_val = in->control; - } - goto handle_err; - } - - if (auto rerr = handle.read<8>(&in->length, shouldACK); - rerr == BAD_PARITY) { - err.errno = BAD_LENGTH_PARITY; - if (print.verbose) { - err.read_val = in->length; - } - goto handle_err; - } - - if (in->length == 0 || in->length > MAXMSGLEN) { - err.errno = BAD_LENGTH_RANGE; - err.val = in->length; - goto handle_err; - } - - for (uint8_t i = 0; i < in->length; i++) { - if (auto rerr = handle.read<8>(&in->data[i], shouldACK); - rerr == BAD_PARITY) { - err.errno = BAD_DATA_PARITY; - if (print.verbose) { - err.read_val = in->data[i]; - } - goto handle_err; - } - } - } // destroy handle - - if (false) { - handle_err:; - RS232_Print("ERR(read): "); - switch (err.errno) { - case BAD_STARTBIT: RS232_Print("bad start bit (other)"); break; - case STARTBIT_TOO_SHORT: RS232_Print("bad start bit (short)"); break; - case STARTBIT_TOO_LONG: RS232_Print("bad start bit (long)"); break; - case BAD_CONTROLLER_PARITY: - RS232_Print("reading controller addr."); - goto VERBOSE; - case BAD_PERIPHERAL_PARITY: - RS232_Print("reading peripheral addr."); - goto VERBOSE; - case BAD_CONTROL_PARITY: RS232_Print("reading control"); goto VERBOSE; - case BAD_LENGTH_PARITY: RS232_Print("reading length"); goto VERBOSE; - case BAD_LENGTH_RANGE: - RS232_Print("bad length 0x"); - RS232_PrintHex4(err.val); - break; - case BAD_DATA_PARITY: - RS232_Print("reading data"); - goto VERBOSE; - VERBOSE: - if (print.verbose) { - RS232_Print("; read 0x"); - RS232_PrintHex(err.read_val); - RS232_Print(" and got bad parity "); - RS232_PrintHex4(err.parity); - } - } - RS232_Print("\n"); - } - - // Only print if some data has been correctly received - if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { - if (err.errno > BAD_DATA_PARITY) - in->length = 0; - AVCLAN_printframe(in, print.binary); - } - - return err.errno; -} - -Peripheral::Error::Send Peripheral::send(const AVCLAN_frame_t *out, - log_t print) { - struct errtype { - // Error enum is ordered such that a lower numeric value corresponds to - // more success - Error::Send errno; - uint8_t val; - } err = {}; - - using enum Error::Send; - auto NAK = Bus::Error::Send::NAK; - - if (AVCLAN_ismuted()) { - err.errno = MUTED; - goto handle_err; - } - - { // bound handle lifetime - auto handle = bus.get(); - - if (!handle.sendstartbit()) { - // Some other device is already driving the bus - err.errno = BUSY; - goto handle_err; - } - - handle.send<1>(static_cast(out->is_unicast), false); - - handle.send<12>(out->controller_addr, false); - - if (auto serr = handle.send<12>(out->controller_addr, out->is_unicast); - serr == NAK) { - err.errno = NAK_ADDRESS; - goto handle_err; - } - - if (auto serr = handle.send<4>(out->control, out->is_unicast); - serr == NAK) { - err.errno = NAK_CONTROL; - goto handle_err; - } - - if (auto serr = handle.send<8>(out->length, out->is_unicast); serr == NAK) { - err.errno = NAK_MESSAGE_LENGTH; - goto handle_err; - } - - for (uint8_t i = 0; i < out->length; i++) { - // Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem - // necessary (i.e. This deviates from the previous broadcast specific - // function that sent an extra `1` bit after each byte/parity) - // Explanation for why audio-group broadcast state report isn't working? - if (auto serr = handle.send<8>(out->data[i], out->is_unicast); - serr == NAK) { - err.errno = NAK_DATA; - err.val = i; - goto handle_err; - } - } - } // destroy handle - - // back to read mode - if (false) { - handle_err:; - RS232_Print("Error"); - switch (err.errno) { - case MUTED: RS232_Print(": Device muted"); break; - case BUSY: RS232_Print(": Busy bus"); break; - case NAK_ADDRESS: - case NAK_CONTROL: - case NAK_MESSAGE_LENGTH: - case NAK_DATA: - RS232_Print(" NAK: "); - switch (err.errno) { - case NAK_ADDRESS: RS232_Print("address"); break; - case NAK_CONTROL: RS232_Print("Control"); break; - case NAK_MESSAGE_LENGTH: RS232_Print("Message length"); break; - case NAK_DATA: - RS232_Print(" data["); - RS232_PrintDec(err.val); - RS232_Print("]"); - break; - case MUTED: - case BUSY: __builtin_unreachable(); - } - break; - } - RS232_Print("\n"); - } - - if (print.print) - AVCLAN_printframe(out, print.binary); - - return err.errno; -} - -} // namespace avclan diff --git a/src/avclan/peripheral.hpp b/src/avclan/peripheral.hpp index 7321604..86656ac 100644 --- a/src/avclan/peripheral.hpp +++ b/src/avclan/peripheral.hpp @@ -6,48 +6,149 @@ #include "avclan_defs.h" #include "bus.hpp" +#include "device.hpp" #include +#include +#include namespace avclan { -class Peripheral { +template class Peripheral { public: - struct Error { - // Error enums are ordered such that a lower numeric value corresponds to - // more progress/success before an error occured, with 0 being no errors - enum class Read : uint8_t { - BAD_DATA_PARITY = 0x01, - BAD_LENGTH_RANGE, - BAD_LENGTH_PARITY, - BAD_PERIPHERAL_PARITY, - BAD_CONTROLLER_PARITY, - BAD_CONTROL_PARITY, - STARTBIT_TOO_SHORT = - static_cast(Bus::Error::Read::STARTBIT_TOO_SHORT), - STARTBIT_TOO_LONG = - static_cast(Bus::Error::Read::STARTBIT_TOO_LONG), - BAD_STARTBIT = static_cast(Bus::Error::Read::BAD_STARTBIT), - }; + using Error = detail::Error; - enum class Send : uint8_t { - NAK_DATA = 0x01, - NAK_MESSAGE_LENGTH, - NAK_CONTROL, - NAK_ADDRESS, - BUSY, - MUTED, - }; - }; - - Peripheral(Bus bus, uint16_t address) : bus{bus}, address{address} { + Peripheral(Bus bus, uint16_t address) : bus{bus}, address_{address} { bus.init(); + (std::get(devices_).init(), ...); } - Error::Read read(AVCLAN_frame_t *in, log_t print); - Error::Send send(const AVCLAN_frame_t *out, log_t print); + uint16_t address() const { return address_; }; + + Error::Read read(AVCLAN_frame_t *in, log_t print) { + return bus.read(address_, in, print); + }; + Error::Send send(const AVCLAN_frame_t *out, log_t print) { + return bus.send(out, print); + }; + +#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) + + void route(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { + out->reaction = 0; + + if (AVCLAN_ismuted() || in->length < 3) + return; + + // 0xFF placeholders are variant bytes filled by writing directly to + // out->data[N] after memcpy. + static const uint8_t lancheck_resp[] = {0x00, dev_COMM_CTRL, dev_LAN, 0xFF, + 0xFF}; + + out->controller_addr = address_; + out->peripheral_addr = controller; + out->control = 0xF; + + const uint8_t *data = in->data; + const uint8_t b0 = *data++; + const uint8_t b1 = *data++; + const uint8_t b2 = *data++; + uint8_t b3 = 0; + if (in->length > 3) // the shortest known/valid messages are 3 bytes long + b3 = *data++; + + if (!in->is_unicast) { + // Broadcast: bytes are (from, to, action, [extra...]). + // peripheral_addr unchecked — always 0xFFF or 0x1FF in known traffic. + switch (PACK3(b0, b1, b2)) { + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Scan_Req): + out->length = sizeof(lancheck_resp); + out->is_unicast = true; + memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); + out->data[3] = Lancheck_Scan_Resp; + out->data[4] = 0x01; + out->reaction = 1; + break; + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Req): + out->length = sizeof(lancheck_resp); + out->is_unicast = true; + memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); + out->data[3] = Lancheck_Resp; + out->data[4] = 0x00; + out->reaction = 1; + break; + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_End_Req): + out->is_unicast = true; + out->length = sizeof(lancheck_resp) - 1; + memcpy(out->data, lancheck_resp, out->length); + out->data[3] = Lancheck_End_Resp; + out->reaction = 1; + break; + case PACK3(dev_COMM_v1, dev_COMM_CTRL, Current_Function): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, Current_Function): + ((Devs::id == b3 ? std::get(devices_).enable(out), 0 : 0), ...); + break; + case PACK3(dev_COMM_v1, dev_COMM_CTRL, Ping_Req): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, Ping_Req): { + out->is_unicast = true; + const uint8_t ping_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, + Ping_Resp, 0xFF, b3}; + out->length = sizeof(ping_resp); + memcpy(out->data, ping_resp, sizeof(ping_resp)); + out->reaction = 1; + break; + } + case PACK3(dev_COMM_v1, dev_COMM_CTRL, List_Functions_Req): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, List_Functions_Req): { + controller = in->controller_addr; + out->peripheral_addr = controller; + out->is_unicast = true; + const uint8_t list_functions_resp[] = { + 0x00, dev_COMM_CTRL, dev_COMM_v1, List_Functions_Resp, + dev_CD_CHANGER}; + out->length = sizeof(list_functions_resp); + memcpy(out->data, list_functions_resp, sizeof(list_functions_resp)); + out->reaction = 1; + break; + } + // case Restart_Lan: not handled + } + } else if (in->peripheral_addr == address_ && b0 == 0x00) { + ((Devs::id == b2 ? device_preroute(std::get(devices_), in, out), + 0 : 0), + ...); + } + } + + void react(AVCLAN_frame_t *out, Error::Send err) { + if (((Devs::id == out->owning_device) || ...)) + ((Devs::id == out->owning_device + ? std::get(devices_).react(out, err), + 0 : 0), + ...); + else + out->reaction = 0; + } + +#undef PACK3 + + template void poll_devices(F &&fun) { + (poller(std::get(devices_), fun), ...); + } private: + template void poller(Dev &dev, F &&fun) { + if (dev.pending() && fun(dev)) + dev.resolvepending(); + } + template + void device_preroute(Dev dev, const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { + out->owning_device = Dev::id; + dev.handle(in, out); + } + Bus bus; - const uint16_t address; + uint16_t controller = 0; + const uint16_t address_; + std::tuple devices_; }; } // namespace avclan diff --git a/src/avclan/statustimer.h b/src/avclan/statustimer.h index ec615d3..79729ea 100644 --- a/src/avclan/statustimer.h +++ b/src/avclan/statustimer.h @@ -12,13 +12,13 @@ extern "C" { #endif // One-time hardware bring-up. Leaves the tick disabled. -void statustimer_init(void); +void statustimer_init(void *ptr, void(clbk)(void *), bool(isplay)(void *)); // Reset the count so the next tick is ~1 s out, and enable the tick. void statustimer_reset(void); // Enable / disable the ~1 Hz tick. -void statustimer_enable(void); +void statustimer_restore(void); void statustimer_disable(void); extern volatile bool tick_pending; diff --git a/src/avclan/target/avr-attiny3216/phy_avr.c b/src/avclan/target/avr-attiny3216/phy_avr.c index 1f40781..a858103 100644 --- a/src/avclan/target/avr-attiny3216/phy_avr.c +++ b/src/avclan/target/avr-attiny3216/phy_avr.c @@ -10,7 +10,6 @@ #include #include "avclan_phy.h" -#include "cdchanger.h" // AVCLAN_isPlaying (startEvent) #include "com232.h" // RS232_setRxInterrupt (guard); RS232_Print (Measure) #include "media_avr.h" // mediacontrol_syncDuringMask (guard) #include "statustimer.h" // statustimer_enable/disable (guard) @@ -414,8 +413,7 @@ void AVCLAN_stopEvent() { // Re-enable serial and periodic interrupts after a bus transaction. void AVCLAN_startEvent() { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing - statustimer_enable(); + statustimer_restore(); // Reenable status interrupt if currently playing RS232_setRxInterrupt(true); } } diff --git a/src/avclan/target/avr-attiny3216/statustick_avr.c b/src/avclan/target/avr-attiny3216/statustick_avr.c index aab6950..40f7e28 100644 --- a/src/avclan/target/avr-attiny3216/statustick_avr.c +++ b/src/avclan/target/avr-attiny3216/statustick_avr.c @@ -8,7 +8,6 @@ #include #include -#include "cdchanger.h" #include "statustimer.h" // Measured wall-clock duration (in ms) of one nominal 32768-tick RTC period, @@ -26,7 +25,11 @@ static constexpr uint16_t rtc_status_per = (uint16_t)(32768UL * 1000UL / RTC_STATUS_PERIOD_MS) - 1U; -void statustimer_init() { +static void* changer = nullptr; +static void (*increment)(void *) = nullptr; +static bool (*isplaying)(void *) = nullptr; + +void statustimer_init(void *ptr, void (inc)(void *), bool (isplay)(void *)) { // Setup RTC as a ~1 sec periodic timer via the normal counter's overflow. // Use the RTC directly (not PIT) to tune the status report interval closer to // 1 sec (internal osc may be slightly off) @@ -37,6 +40,10 @@ void statustimer_init() { RTC.INTCTRL = 0; loop_until_bit_is_clear(RTC_STATUS, RTC_CTRLABUSY_bp); RTC.CTRLA = RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm; + + changer = ptr; + increment = inc; + isplaying = isplay; } void statustimer_reset() { @@ -48,8 +55,10 @@ void statustimer_reset() { } } -void statustimer_enable() { RTC.INTCTRL |= RTC_OVF_bm; } - +void statustimer_restore() { + if (isplaying(changer)) + RTC.INTCTRL |= RTC_OVF_bm; +} void statustimer_disable() { RTC.INTCTRL &= ~RTC_OVF_bm; } // Set once per overflow; consumed by the app via statustimer_tickPending(). @@ -57,7 +66,7 @@ volatile bool tick_pending = false; // Periodic interrupt with a ~1 sec period; only enabled while playing. ISR(RTC_CNT_vect) { - AVCLAN_incrementTime(); + increment(changer); tick_pending = true; RTC.INTFLAGS = RTC_OVF_bm; } diff --git a/src/sniffer.cc b/src/sniffer.cc index 47bd927..b16088d 100644 --- a/src/sniffer.cc +++ b/src/sniffer.cc @@ -9,6 +9,7 @@ #include "avclandrv.h" #include "board.h" +#include "cdchanger.hpp" #include "com232.h" #include "peripheral.hpp" #include "queue.hpp" @@ -62,18 +63,14 @@ int main() { uint8_t data_tmp[MAXMSGLEN + sizeof(AVCLAN_frame_t)]; uint8_t seqIdx = 0; // current index in data_tmp - uint8_t failedStatusReports = 0; - // Temporary, direct access is questionable since cache has ownership for (auto &frame : frames) { frame.control = 0x0f; } - const AVCLAN_frame_t *lastStatus = nullptr; - avclan::Bus phy; - avclan::Peripheral cd_changer(phy, 0x360); - using Error = avclan::Peripheral::Error; + avclan::Peripheral cd_changer(phy, 0x360); + using Error = decltype(cd_changer)::Error; Setup(); print_help(); @@ -91,42 +88,33 @@ int main() { } } - if (auto in = incoming.peek()) { + if (const auto *in = incoming.peek()) { if (auto out = cache.pop()) { - AVCLAN_handleframe(in, out.get()); + cd_changer.route(in, out.get()); incoming.pop(); - if (out->reaction) + if (out->reaction > 0) outgoing.push(std::move(out)); } else { RS232_Print("!! Unable to respond; cache is empty !!\n"); } } - if (statustimer_tickPending()) { + cd_changer.poll_devices([&](auto dev) { if (auto status = cache.pop()) { - lastStatus = status.get(); - AVCLAN_generateStatus(status.get(), true, dev_STATUS); - status->reaction = r_SendOnly; + dev.emit(status.get()); outgoing.push(std::move(status)); - statustimer_clearTick(); + return true; } - } + return false; + }); if (auto out = outgoing.pop()) { auto err = cd_changer.send( out.get(), (log_t){.print = printAllFrames, .binary = printBinary}); - if (err == Error::Send{0x00} && (reaction_t)out->reaction > r_SendOnly) { - AVCLAN_statemachine(out.get()); - if (out->reaction) - outgoing.push(std::move(out)); - - } else if (err == Error::Send::NAK_ADDRESS && out.get() == lastStatus && - ++failedStatusReports > 1) { - failedStatusReports = 0; - AVCLAN_stopPlaying(); // Disable periodic updates if e.g. no-one's - // listening (car was turned off?) - } + cd_changer.react(out.get(), err); + if (out->reaction > 0) + outgoing.push(std::move(out)); } // Key handler @@ -151,7 +139,7 @@ int main() { case 'E': // Beep if (auto out = cache.pop()) { out->is_unicast = true; - out->controller_addr = DEVICE_ADDR; + out->controller_addr = cd_changer.address(); out->peripheral_addr = HU_ADDR; { const uint8_t beep[] = {0x00, dev_CD_CHANGER, dev_BEEP_SPEAKERS, @@ -159,14 +147,14 @@ int main() { out->length = sizeof(beep); memcpy(out->data, beep, sizeof(beep)); } - out->reaction = r_SendOnly; + out->reaction = 1; outgoing.push(std::move(out)); } break; case 'P': if (auto out = cache.pop()) { out->is_unicast = true; - out->controller_addr = DEVICE_ADDR; + out->controller_addr = cd_changer.address(); out->peripheral_addr = HU_ADDR; { const uint8_t play[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, @@ -174,7 +162,7 @@ int main() { out->length = sizeof(play); memcpy(out->data, play, sizeof(play)); } - out->reaction = r_Ejection; + out->reaction = avclan::CDChanger::reaction_t::r_Ejection; outgoing.push(std::move(out)); } break; @@ -237,7 +225,7 @@ int main() { if (data_tmp[seqIdx] == 0x17) { if (auto out = cache.pop()) { if (!AVCLAN_parseframe(data_tmp, --seqIdx, out.get())) { - out->reaction = r_SendOnly; + out->reaction = 1; outgoing.push(std::move(out)); } } @@ -248,11 +236,11 @@ int main() { } else { if (auto out = cache.pop()) { out->is_unicast = seqIsUnicast; - out->controller_addr = DEVICE_ADDR; + out->controller_addr = cd_changer.address(); out->peripheral_addr = seqIsUnicast ? HU_ADDR : 0x1FF; out->length = seqIdx; memcpy(out->data, data_tmp, seqIdx); - out->reaction = r_SendOnly; + out->reaction = 1; outgoing.push(std::move(out)); } printAllFrames = lastPrintAllFrames; @@ -298,7 +286,6 @@ namespace { void Setup() { board_init(); // clock + GPIO bring-up (target-specific) RS232_Init(); - AVCLAN_init(); board_interruptsEnable(); }