Files
Toyota-AVC-LAN/src/avclan/cdchanger.cc
T
2026-07-06 20:31:22 -07:00

435 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.h"
#include "cdchanger.hpp"
#include "device.hpp"
#include "frame.hpp"
#include "hal/cd_timer.h"
#include "hal/media.h"
namespace {
using namespace avclan;
constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
to_underlying(Device::STATUS),
to_underlying(Action::Loading_Status),
0x00,
0x01,
0x00,
0x01,
0x00,
0x01,
0x02};
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr int TIME_SKIP = 15; // seconds
constexpr int TWODIGIT_MAX = 99;
/* Pack a 0TWODIGIT_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() {
media_init();
cdtimer_init(this, &incrementTime_callback, &isPlaying_callback);
}
void CDChanger::handle(const Frame *in, Frame *out) {
const uint8_t *data = &in->data[1];
const auto from = static_cast<Device>(*data++);
/* const auto to = */ data++;
const auto action = static_cast<Action>(*data++);
static const uint8_t function_change_resp[] = {
0x00, to_underlying(Device::CD_CHANGER), to_underlying(from), 0xFF, 0x01};
using enum Action;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
// 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] = to_underlying(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] = to_underlying(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, Device::CD_CHANGER, Device::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, to_underlying(Device::CD_CHANGER),
to_underlying(Device::CMD_SW),
to_underlying(Insertion), 0x01};
out->length = sizeof(msg);
memcpy(out->data, msg, sizeof(msg));
}
out->reaction = r_SendOnly;
}
break;
}
case Initial_Report_Req: {
out->is_unicast = true;
// No knowledge/understanding of field meaning/interpretation
const uint8_t cdinitreport_resp[] = {0x00,
to_underlying(Device::CD_CHANGER),
to_underlying(from),
to_underlying(Initial_Report_Resp),
0x01,
0x31,
0x10,
0x01,
0x01};
out->length = sizeof(cdinitreport_resp);
memcpy(out->data, cdinitreport_resp, sizeof(cdinitreport_resp));
out->reaction = r_SendOnly;
break;
}
case Playback_Req:
out->data[0] = 0x00;
out->data[1] = to_underlying(Device::CD_CHANGER);
out->data[2] = to_underlying(from);
out->data[3] = to_underlying(Playback_Resp);
out->length = WIRE_SIZE + 4;
serialize(&out->data[4]);
out->is_unicast = true;
out->reaction = r_SendOnly;
break;
case Loading_Req:
out->data[0] = 0x00;
out->length = sizeof(cdloading_resp) + 1;
memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp));
out->data[2] = to_underlying(from);
out->data[3] = to_underlying(Loading_Resp);
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, Device::CMD_SW);
media_action(MediaAction::Track_Next);
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;
media_action(MediaAction::Track_Prev);
}
mins = 0xff;
secs = 0x7f;
flags2 = 0xc0;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_TrackChange;
break;
case Track_Fast_Forward: {
state |= SEEKING;
secs += TIME_SKIP;
if (secs > 60) {
secs -= 60;
++mins;
}
generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Forward);
cdtimer_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, Device::CMD_SW);
media_action(MediaAction::Skip_Backward);
cdtimer_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, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Disable_Random:
flags &= ~RANDOM;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Enable_Repeat:
flags |= REPEAT;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Disable_Repeat:
flags &= ~REPEAT;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Enable_Disk_Random:
flags |= DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Disable_Disk_Random:
flags &= ~DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Enable_Disk_Repeat:
flags |= DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
case CD_Disable_Disk_Repeat:
flags &= ~DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport;
break;
default: break;
}
#pragma GCC diagnostic pop
}
void CDChanger::react(Frame *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,
to_underlying(Device::COMM_CTRL),
to_underlying(Device::COMMUNICATION_V1),
to_underlying(Action::Insertion),
to_underlying(Device::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] = to_underlying(Device::STATUS);
out->data[2] = to_underlying(Action::Loading_Status);
out->reaction = r_SendOnly;
break;
case r_TrackChange:
setTime(0, 0);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is
// ~1 sec from now
[[fallthrough]];
case r_NormalizeState:
normalizeState();
generateStatus(out, false, Device::STATUS);
out->reaction = r_SendOnly;
break;
case r_StartPlaying:
normalizeState();
generateStatus(out, false, Device::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, false, Device::STATUS);
out->reaction = r_SendOnly;
break;
case r_SendOnly: [[fallthrough]];
case r_Nothing: [[fallthrough]];
default: out->reaction = r_Nothing;
}
}
void CDChanger::enable(Frame *out) {
if (!isPlaying()) {
if (mins > TWODIGIT_MAX)
mins = 0;
if (secs > TWODIGIT_MAX)
secs = 0;
state = SEEKING | SEEKING_TRACK;
flags2 = 0xc0;
generateStatus(out, false, Device::STATUS);
out->reaction = r_StartPlaying;
}
}
bool CDChanger::pending() { return cdtimer_pending(); }
void CDChanger::resolvepending() { cdtimer_clear(); }
void CDChanger::emit(Frame *out, uint16_t peripheral) {
out->owning_device = id; // so react() routes r_StateReport back here
out->peripheral_addr = peripheral;
generateStatus(out, false, Device::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)
media_action(MediaAction::Play);
havePlayed |= true;
playing = true;
cdtimer_reset();
}
void CDChanger::stopPlaying() {
cdtimer_disable();
playing = false;
media_action(MediaAction::Pause);
}
// Serialize cd_status into the wire format.
// track/mins/secs, 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(Frame *status, bool is_unicast,
Device 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++ = to_underlying(Device::CD_CHANGER);
*data++ = to_underlying(to);
*data++ = to_underlying(Action::Playback_Status);
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;
}
#ifndef NDEBUG
void CDChanger::media_action(MediaAction action) { ::media_action(action); }
bool CDChanger::media_busy() const { return ::media_busy(); };
void CDChanger::mic_toggle() { media_mic_toggle(); };
#endif
} // namespace avclan