mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-06 17:03:17 +00:00
435 lines
13 KiB
C++
435 lines
13 KiB
C++
// 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 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() {
|
||
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
|