mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-06 17:03:17 +00:00
Parameterise Peripheral by its Devices and add CDChanger as one
This commit is contained in:
+26
-18
@@ -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
|
||||
|
||||
+2
-3
@@ -8,6 +8,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
add_compile_options(
|
||||
-Wall -Wswitch-enum -Werror
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
|
||||
$<$<CONFIG:Debug>:-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
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
@@ -61,38 +61,3 @@ void AVCLan_Measure(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
template <class T, auto N> avclan_bit_t AVCLAN_sendbits(T bits);
|
||||
template <class T, auto N> 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 <auto N>
|
||||
requires(N <= 8)
|
||||
avclan_bit_t AVCLAN_sendbits(uint8_t bits) {
|
||||
return AVCLAN_sendbitsi(&bits, N);
|
||||
}
|
||||
template <auto N>
|
||||
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 <auto N>
|
||||
requires(N <= 8)
|
||||
avclan_bit_t AVCLAN_readbits(uint8_t *bits) {
|
||||
return static_cast<avclan_bit_t>(AVCLAN_readbitsi(bits, N));
|
||||
}
|
||||
template <auto N>
|
||||
requires(N <= 16)
|
||||
avclan_bit_t AVCLAN_readbits(uint16_t *bits) {
|
||||
return static_cast<avclan_bit_t>(AVCLAN_readbitsl(bits, N));
|
||||
}
|
||||
template <> inline avclan_bit_t AVCLAN_readbits<8>(uint8_t *byte) {
|
||||
return static_cast<avclan_bit_t>(AVCLAN_readbyte(byte));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
|
||||
// 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 <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -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"
|
||||
|
||||
+268
-5
@@ -3,28 +3,291 @@
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// 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<uint8_t>(handle.readstartbit())};
|
||||
if (static_cast<bool>(err.errno))
|
||||
goto handle_err;
|
||||
|
||||
handle.read<1>(&tmp, false);
|
||||
in->is_unicast = tmp;
|
||||
|
||||
if (auto rerr = handle.read<ADDR_WIDTH>(&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<ADDR_WIDTH>(
|
||||
&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<uint8_t>(out->is_unicast), false);
|
||||
|
||||
handle.send<ADDR_WIDTH>(out->controller_addr, false);
|
||||
|
||||
if (auto serr =
|
||||
handle.send<ADDR_WIDTH>(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(); };
|
||||
|
||||
+20
-25
@@ -49,31 +49,24 @@
|
||||
|
||||
#include <concepts>
|
||||
|
||||
#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 <auto N, class T> avclan_bit_t sendbits(T bits);
|
||||
template <auto N, class T> 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<avclan_bit_t>(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<avclan_bit_t>(bits & 1U)};
|
||||
AVCLAN_sendbit(bit);
|
||||
return bit;
|
||||
};
|
||||
template <> inline avclan_bit_t Bus::Handle::readbits<8>(uint8_t *byte) {
|
||||
return static_cast<avclan_bit_t>(AVCLAN_readbyte(byte));
|
||||
template <> inline avclan_bit_t Bus::Handle::readbits<8>(uint8_t *bits) {
|
||||
return static_cast<avclan_bit_t>(AVCLAN_readbyte(bits));
|
||||
};
|
||||
|
||||
} // namespace avclan
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#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<avclan::CDChanger *>(self)->incrementTime();
|
||||
}
|
||||
|
||||
extern "C" bool isPlaying_callback(void *self) {
|
||||
return static_cast<avclan::CDChanger *>(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<devices>(*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<bool>(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<reaction_t>(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
|
||||
@@ -1,85 +0,0 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// 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 <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,93 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,25 @@
|
||||
// copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "avclan.hpp"
|
||||
#include "avclan_defs.h"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace avclan {
|
||||
template <class T>
|
||||
concept Device = requires { std::integral_constant<uint8_t, T::id>{}; } &&
|
||||
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<bool>;
|
||||
dev.resolvepending();
|
||||
dev.emit(out);
|
||||
};
|
||||
} // namespace avclan
|
||||
@@ -1,243 +0,0 @@
|
||||
// copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
// copyright (C) 2007 Louis Frigon
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// 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 <cstdint>
|
||||
|
||||
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<uint8_t>(handle.readstartbit())};
|
||||
if (static_cast<uint8_t>(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<uint8_t>(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
|
||||
+132
-31
@@ -6,48 +6,149 @@
|
||||
|
||||
#include "avclan_defs.h"
|
||||
#include "bus.hpp"
|
||||
#include "device.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
|
||||
namespace avclan {
|
||||
class Peripheral {
|
||||
template <Device... Devs> 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<uint8_t>(Bus::Error::Read::STARTBIT_TOO_SHORT),
|
||||
STARTBIT_TOO_LONG =
|
||||
static_cast<uint8_t>(Bus::Error::Read::STARTBIT_TOO_LONG),
|
||||
BAD_STARTBIT = static_cast<uint8_t>(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<Devs>(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<Devs>(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<Devs>(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<Devs>(devices_).react(out, err),
|
||||
0 : 0),
|
||||
...);
|
||||
else
|
||||
out->reaction = 0;
|
||||
}
|
||||
|
||||
#undef PACK3
|
||||
|
||||
template <class F> void poll_devices(F &&fun) {
|
||||
(poller(std::get<Devs>(devices_), fun), ...);
|
||||
}
|
||||
|
||||
private:
|
||||
template <Device Dev, class F> void poller(Dev &dev, F &&fun) {
|
||||
if (dev.pending() && fun(dev))
|
||||
dev.resolvepending();
|
||||
}
|
||||
template <Device Dev>
|
||||
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<Devs...> devices_;
|
||||
};
|
||||
} // namespace avclan
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <util/atomic.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <stdint.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
+21
-34
@@ -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<avclan::CDChanger> 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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user