14 Commits

Author SHA1 Message Date
Allen Hill eab535db98 fixup 7871e92 2026-08-03 13:30:14 -07:00
Allen Hill f3d0d99ce4 Update Disable_Function_Req response 2026-08-01 13:15:06 -07:00
Allen Hill 76f5da0125 Restore dynamic from device in function change resp 2026-08-01 13:14:38 -07:00
Allen Hill 7871e92dbe Fix Debug builds 2026-07-31 17:53:31 -07:00
Allen Hill fb17b7e75b Update gitignore and silence some tidy warnings 2026-07-25 11:22:33 -07:00
Allen Hill ae539e27f6 Fix some overflow behavior when reading input msgs 2026-07-23 12:41:00 -07:00
Allen Hill a99fe56aae Fix intermittent, early boot short start bit errors 2026-07-21 13:22:19 -07:00
Allen Hill 909915730b Not all error conditions print a message 2026-07-21 12:52:25 -07:00
Allen Hill 5bbbb6c011 Party should be ~private 2026-07-21 12:51:49 -07:00
Allen Hill 8950bd6917 Make b3 const 2026-07-21 12:51:18 -07:00
Allen Hill 56936aed10 Switch to using references where possible 2026-07-17 14:31:54 -07:00
Allen Hill 5208e083f3 Redesign (simplify) top-level avclan interface to use expected/nullable unique_ptr
- Peripheral::read/send now return `expected<unique_ptr<Frame>, Error>`
for a unified success/error interface.
- Peripheral::route returns `expected<unique_ptr<Frame>, Error>` to
distinguish intent: (intentional) non-response vs unable to respond
- Other functions with optional message semantics (handle,poll,react)
return a unique_ptr whose ownership-state indicates response intent
(i.e. send new message)

Bundled necessary changes:
- Switch Frame allocation from global to local (enabled via member
  function new/delete)
- Add new header-library tl::expected to shim avr-libstdcpp

Unrelated: Defensive `continue` added after `route`, to reduce Bus
activity check latency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:47:47 -07:00
Allen Hill 200a225add Unify frame metadata setting (addrs and owning_device) 2026-07-16 13:35:25 -07:00
Allen Hill 568c2af9e6 Only check tracked files and don't print on empty lint 2026-07-16 13:35:06 -07:00
24 changed files with 821 additions and 316 deletions
+2
View File
@@ -16,6 +16,8 @@ Checks: |
-modernize-avoid-c-arrays, -modernize-avoid-c-arrays,
-modernize-use-std-print, -modernize-use-std-print,
-readability-magic-numbers, -readability-magic-numbers,
-readability-function-cognitive-complexity,
-misc-non-private-member-variables-in-classes
WarningsAsErrors: "" WarningsAsErrors: ""
ExcludeHeaderFilterRegex: 'out/build' ExcludeHeaderFilterRegex: 'out/build'
HeaderFilterRegex: '^src/.*' HeaderFilterRegex: '^src/.*'
+1
View File
@@ -12,3 +12,4 @@ __pycache__/
~* ~*
\#* \#*
/CMakeUserPresets.json /CMakeUserPresets.json
.cache/
+91 -4
View File
@@ -7,12 +7,17 @@ set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_compile_options( add_compile_options(
-Wall -Wswitch-enum -Werror -Wall -Wswitch-enum -Werror
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics> $<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions> $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti> $<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer> $<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>) $<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
@@ -71,13 +76,95 @@ add_library(avclan STATIC
target_include_directories(avclan PUBLIC target_include_directories(avclan PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/avclan) ${CMAKE_CURRENT_SOURCE_DIR}/src/avclan)
add_executable(mockingboard
src/sniffer.cc
)
target_link_libraries(mockingboard avclan)
# Report firmware section sizes on every build, when the toolchain provides a
# size tool (CMAKE_SIZE). A standalone custom target (rather than a POST_BUILD
# command on mockingboard) so it prints even when the link is up to date:
# custom targets with no output are always considered stale and re-run.
# add_dependencies orders it after the link.
if(CMAKE_SIZE)
add_custom_target(mockingboard-size ALL
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard>
COMMENT "Section sizes: mockingboard firmware"
VERBATIM
)
add_dependencies(mockingboard-size mockingboard)
endif()
# Pull in the selected hardware target: its port sources, per-target headers, # Pull in the selected hardware target: its port sources, per-target headers,
# hardware-specific compile options/definitions, device-pack handling, and the # hardware-specific compile options/definitions, device-pack handling, and the
# flashing target. Added after the targets above so it can extend them. # flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET}) add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
add_executable(mockingboard # --- Polyfill headers for std facilities the toolchain may lack -------------
src/sniffer.cc # tl::expected / tl::optional back-fill std::expected / std::optional (with
) # C++23 monadic ops) when the build's stdlib predates them. Runs after the port
# subdirectory to provide a port-supplied freestanding stdlib via
# CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles)
include(FetchContent)
find_program(GIT_EXECUTABLE git REQUIRED)
target_link_libraries(mockingboard avclan) if(TARGET libstdcpp)
get_target_property(_stdlib_incs libstdcpp INTERFACE_INCLUDE_DIRECTORIES)
set(CMAKE_REQUIRED_INCLUDES ${_stdlib_incs})
endif()
check_cxx_source_compiles("
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#error no std::expected
#endif
int main() { return 0; }" HAVE_STD_EXPECTED)
# check_cxx_source_compiles("
# #include <version>
# #if !defined(__cpp_lib_optional) || __cpp_lib_optional < 202110L
# #error no C++23 std::optional
# #endif
# int main() { return 0; }" HAVE_STD_OPTIONAL)
unset(CMAKE_REQUIRED_INCLUDES)
if(NOT HAVE_STD_EXPECTED)
FetchContent_Declare(
tl_expected
GIT_REPOSITORY https://github.com/TartanLlama/expected.git
GIT_TAG 1770e3559f2f6ea4a5fb4f577ad22aeb30fbd8e4
PATCH_COMMAND ${CMAKE_COMMAND}
-DGIT_EXECUTABLE=${GIT_EXECUTABLE}
-DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-expected-no-exception-header.patch
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
SYSTEM
)
set(EXPECTED_BUILD_TESTS OFF)
set(EXPECTED_BUILD_PACKAGE OFF)
FetchContent_MakeAvailable(tl_expected)
add_library(tl_expected_hdr INTERFACE)
target_include_directories(tl_expected_hdr
INTERFACE ${tl_expected_SOURCE_DIR}/include)
target_link_libraries(avclan PUBLIC tl_expected_hdr)
endif()
# std/tl::optional not currently used
# if(NOT HAVE_STD_OPTIONAL)
# FetchContent_Declare(
# tl_optional
# GIT_REPOSITORY https://github.com/TartanLlama/optional.git
# GIT_TAG 3a1209de8370bf5fe16362934956144b49591565
# PATCH_COMMAND ${CMAKE_COMMAND}
# -DGIT_EXECUTABLE=${GIT_EXECUTABLE}
# -DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-optional-no-exception-header.patch
# -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
# SYSTEM
# )
# set(OPTIONAL_BUILD_TESTS OFF)
# set(OPTIONAL_BUILD_PACKAGE OFF)
# FetchContent_MakeAvailable(tl_optional)
# add_library(tl_optional_hdr INTERFACE)
# target_include_directories(tl_optional_hdr
# INTERFACE ${tl_optional_SOURCE_DIR}/include)
# target_link_libraries(avclan PUBLIC tl_optional_hdr)
# endif()
+39 -7
View File
@@ -22,7 +22,7 @@
"hidden": true, "hidden": true,
"description": "AVR ATtiny3216 cross-compile toolchain + matching port", "description": "AVR ATtiny3216 cross-compile toolchain + matching port",
"toolchainFile": "${sourceDir}/cmake/avr-gcc-toolchain.cmake", "toolchainFile": "${sourceDir}/cmake/avr-gcc-toolchain.cmake",
"binaryDir": "${sourceDir}/out/build/attiny3216", "binaryDir": "${sourceDir}/out/build/avr-attiny3216",
"cacheVariables": { "cacheVariables": {
"AVCLAN_TARGET": "avr-attiny3216", "AVCLAN_TARGET": "avr-attiny3216",
"FREQSEL": "20MHz", "FREQSEL": "20MHz",
@@ -34,8 +34,6 @@
"name": "debug-base", "name": "debug-base",
"hidden": true, "hidden": true,
"description": "Debug build settings", "description": "Debug build settings",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/out/build",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug" "CMAKE_BUILD_TYPE": "Debug"
} }
@@ -49,13 +47,30 @@
"CMAKE_BUILD_TYPE": "RelWithDebInfo" "CMAKE_BUILD_TYPE": "RelWithDebInfo"
} }
}, },
{
"name": "attiny3216-relwithdebinfo",
"hidden": true,
"description": "ATtiny3216 RelWithDebInfo build,",
"inherits": [
"avr-attiny3216",
"relwithdebinfo-base"
]
},
{
"name": "attiny3216-debug",
"hidden": true,
"description": "ATtiny3216 Debug build",
"inherits": [
"avr-attiny3216",
"debug-base"
]
},
{ {
"name": "attiny3216-relwithdebinfo-usb0", "name": "attiny3216-relwithdebinfo-usb0",
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB0)", "displayName": "ATtiny3216 RelWithDebInfo (ttyUSB0)",
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB0", "description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB0",
"inherits": [ "inherits": [
"avr-attiny3216", "attiny3216-relwithdebinfo",
"relwithdebinfo-base",
"usb0" "usb0"
] ]
}, },
@@ -64,8 +79,25 @@
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB1)", "displayName": "ATtiny3216 RelWithDebInfo (ttyUSB1)",
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB1", "description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB1",
"inherits": [ "inherits": [
"avr-attiny3216", "attiny3216-relwithdebinfo",
"relwithdebinfo-base", "usb1"
]
},
{
"name": "attiny3216-debug-usb0",
"displayName": "ATtiny3216 Debug (ttyUSB0)",
"description": "ATtiny3216 Debug build, program over /dev/ttyUSB0",
"inherits": [
"attiny3216-debug",
"usb0"
]
},
{
"name": "attiny3216-debug-usb1",
"displayName": "ATtiny3216 Debug (ttyUSB1)",
"description": "ATtiny3216 Debug build, program over /dev/ttyUSB1",
"inherits": [
"attiny3216-debug",
"usb1" "usb1"
] ]
} }
+18
View File
@@ -0,0 +1,18 @@
# Idempotent patch driver for FetchContent PATCH_COMMAND steps.
#
# Usage: cmake -DGIT_EXECUTABLE=<git> -DPATCH_FILE=<patch> -P apply-patch.cmake
# (run with CWD = the populated source directory, as PATCH_COMMAND does).
#
# FetchContent re-runs PATCH_COMMAND after its update step on reconfigure, so a
# bare `git apply` fails the second time around. Skip if the patch is already
# applied (the reverse-apply dry run succeeds); otherwise apply it for real.
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --reverse --check --ignore-whitespace
"${PATCH_FILE}"
RESULT_VARIABLE _already_applied
OUTPUT_QUIET ERROR_QUIET)
if(NOT _already_applied EQUAL 0)
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace "${PATCH_FILE}"
COMMAND_ERROR_IS_FATAL ANY)
endif()
+3
View File
@@ -13,6 +13,9 @@
########################################################################## ##########################################################################
find_program(AVR_CC avr-gcc REQUIRED) find_program(AVR_CC avr-gcc REQUIRED)
find_program(AVR_CXX avr-g++ REQUIRED) find_program(AVR_CXX avr-g++ REQUIRED)
# Section-size reporter; consumed by an optional POST_BUILD in the top-level
# CMakeLists (left unset -> no size report) so the top level stays HW-agnostic.
find_program(CMAKE_SIZE avr-size)
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR avr) set(CMAKE_SYSTEM_PROCESSOR avr)
@@ -0,0 +1,36 @@
diff --git a/include/tl/expected.hpp b/include/tl/expected.hpp
index 59e59aa..75857bd 100644
--- a/include/tl/expected.hpp
+++ b/include/tl/expected.hpp
@@ -20,7 +20,7 @@
#define TL_EXPECTED_VERSION_MINOR 3
#define TL_EXPECTED_VERSION_PATCH 1
-#include <exception>
+#include <cstdlib>
#include <functional>
#include <type_traits>
#include <utility>
@@ -222,7 +222,7 @@ static constexpr unexpect_t unexpect{};
#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
#define TL_EXPECTED_THROW_EXCEPTION(e) throw((e));
#else
-#define TL_EXPECTED_THROW_EXCEPTION(e) std::terminate();
+#define TL_EXPECTED_THROW_EXCEPTION(e) std::abort();
#endif
namespace detail {
@@ -1249,11 +1249,11 @@ template <class T, class E> struct expected_default_ctor_base<T, E, false> {
};
} // namespace detail
-template <class E> class bad_expected_access : public std::exception {
+template <class E> class bad_expected_access {
public:
explicit bad_expected_access(E e) : m_val(std::move(e)) {}
- virtual const char *what() const noexcept override {
+ virtual const char *what() const noexcept {
return "Bad expected access";
}
@@ -0,0 +1,21 @@
diff --git a/include/tl/optional.hpp b/include/tl/optional.hpp
index e9c59c2..1ff3d4a 100644
--- a/include/tl/optional.hpp
+++ b/include/tl/optional.hpp
@@ -21,7 +21,6 @@
#define TL_OPTIONAL_VERSION_MINOR 1
#define TL_OPTIONAL_VERSION_PATCH 0
-#include <exception>
#include <functional>
#include <new>
#include <type_traits>
@@ -664,7 +663,7 @@ struct nullopt_t {
static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
nullopt_t::do_not_use{}};
-class bad_optional_access : public std::exception {
+class bad_optional_access {
public:
bad_optional_access() = default;
const char *what() const noexcept { return "Optional has no value"; }
+20 -10
View File
@@ -49,6 +49,7 @@ still bypasses it.
import argparse import argparse
import os import os
import re import re
import subprocess
import sys import sys
# ---- locations ------------------------------------------------------------- # ---- locations -------------------------------------------------------------
@@ -227,10 +228,16 @@ def find_address_spec(root):
def iter_src_files(root): def iter_src_files(root):
for dirpath, _dirs, files in os.walk(os.path.join(root, SRC_REL)): """List source files under src/, via `git ls-files` so paths excluded by
for fn in sorted(files): .gitignore (e.g. the ESP32 port's CMake build directory, which vendors
if fn.endswith(SRC_EXTS): FetchContent'd third-party code) are never walked into."""
yield os.path.join(dirpath, fn) out = subprocess.run(
["git", "-C", root, "ls-files", "-z", "--cached", "--others",
"--exclude-standard", "--", SRC_REL],
capture_output=True, check=True, text=True,
).stdout
paths = sorted(p for p in out.split("\0") if p.endswith(SRC_EXTS))
return [os.path.join(root, p) for p in paths]
# ---- lint ------------------------------------------------------------------ # ---- lint ------------------------------------------------------------------
@@ -471,8 +478,6 @@ def main():
lua_path = os.path.join(root, LUA_REL) lua_path = os.path.join(root, LUA_REL)
lua_lines = read_lines(lua_path) lua_lines = read_lines(lua_path)
specs, have_address = build_specs(root) specs, have_address = build_specs(root)
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
# ---- lint (all modes) ---- # ---- lint (all modes) ----
lint_failed = False lint_failed = False
@@ -487,6 +492,8 @@ def main():
print_lint(spec, LUA_REL, problems) print_lint(spec, LUA_REL, problems)
if args.lint: if args.lint:
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
if lint_failed: if lint_failed:
print("lint: FAILED") print("lint: FAILED")
return 1 return 1
@@ -503,6 +510,8 @@ def main():
has_drift = any(not d.clean() for _s, _e, d in drifts) has_drift = any(not d.clean() for _s, _e, d in drifts)
if args.fix: if args.fix:
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
if lint_failed: if lint_failed:
print("fix aborted: resolve the dissector name problems above first") print("fix aborted: resolve the dissector name problems above first")
return 1 return 1
@@ -525,16 +534,17 @@ def main():
print("fix: applied" if has_drift else "fix: already in sync") print("fix: applied" if has_drift else "fix: already in sync")
return 0 return 0
# ---- default: report ---- # ---- default: report (silent on success -- this is the pre-commit hook) ----
if not (lint_failed or has_drift):
return 0
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
for spec, _entries, drift in drifts: for spec, _entries, drift in drifts:
if not drift.clean(): if not drift.clean():
print("%s drift:" % spec["name"]) print("%s drift:" % spec["name"])
print_drift(drift) print_drift(drift)
if lint_failed or has_drift:
print("out of sync -- run: scripts/sync_avclan_enums.py --fix") print("out of sync -- run: scripts/sync_avclan_enums.py --fix")
return 1 return 1
print("enums in sync with the dissector")
return 0
if __name__ == "__main__": if __name__ == "__main__":
+15 -1
View File
@@ -121,6 +121,12 @@ enum AVCLAN_ENUM_CLASS MediaAction : uint8_t {
}; };
#ifdef __cplusplus #ifdef __cplusplus
enum class Device : uint8_t;
// Sentinel for a frame with no owning device.
// Compile-time collision check lives in device.hpp
inline constexpr Device NoDevice = Device{0xFF};
namespace detail { namespace detail {
struct Error { struct Error {
#endif #endif
@@ -137,9 +143,11 @@ struct Error {
BAD_CONTROLLER_PARITY, BAD_CONTROLLER_PARITY,
BAD_CONTROL_PARITY, BAD_CONTROL_PARITY,
BAD_PARITY, // generic bad parity has max severity BAD_PARITY, // generic bad parity has max severity
STARTBIT_TOO_SHORT, STARTBIT_MISSED,
STARTBIT_MALFORMED,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
POOL_EMPTY, // non-bus error
}; };
enum AVCLAN_ENUM_CLASS Send : uint8_t { enum AVCLAN_ENUM_CLASS Send : uint8_t {
@@ -160,6 +168,12 @@ struct Error {
#ifdef __cplusplus #ifdef __cplusplus
}; };
struct SendError {
Device owning_device;
uint8_t reaction;
Error::Send err;
};
#endif #endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t { enum AVCLAN_ENUM_CLASS Bit : uint8_t {
+33 -16
View File
@@ -31,11 +31,14 @@
#include <concepts> #include <concepts>
#include <cstdio> #include <cstdio>
#include <memory>
#include <new>
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/phy.h" #include "hal/phy.h"
#include "stdshim.hpp"
namespace { namespace {
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
@@ -191,7 +194,8 @@ void Bus::mute(bool mute) {
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
}; };
auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read { auto Bus::read(uint16_t address, Frame::Print print)
-> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype { struct errtype {
Read errno; Read errno;
uint16_t val; uint16_t val;
@@ -199,6 +203,12 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
using enum Read; using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
if (!in) {
err.errno = POOL_EMPTY;
goto handle_err;
}
{ // bound handle lifetime { // bound handle lifetime
auto handle = get(); auto handle = get();
@@ -277,8 +287,12 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
handle_err:; handle_err:;
fputs("ERR(read): ", stdout); fputs("ERR(read): ", stdout);
switch (err.errno) { switch (err.errno) {
case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_TOO_SHORT: fputs("bad start bit (short)", stdout); break; case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
case STARTBIT_MALFORMED:
fputs("malformed start bit (external cause)", stdout);
break;
case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break; case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break;
case BAD_CONTROLLER_PARITY: case BAD_CONTROLLER_PARITY:
fputs("reading controller addr.", stdout); fputs("reading controller addr.", stdout);
@@ -288,29 +302,32 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
goto VERBOSE; goto VERBOSE;
case BAD_CONTROL_PARITY: fputs("reading control", stdout); goto VERBOSE; case BAD_CONTROL_PARITY: fputs("reading control", stdout); goto VERBOSE;
case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE; case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE;
case BAD_LENGTH_RANGE: printf("bad length 0x%02X", err.val); break; case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break;
case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE; case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE;
case BAD_PARITY: case BAD_PARITY:
__builtin_unreachable(); __builtin_unreachable();
VERBOSE: VERBOSE:
if (print.verbose) { if (print.verbose) {
printf("; read 0x%02X", err.val); printf("; read 0x%02X:", err.val);
} }
} }
putchar('\n'); putchar('\n');
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { if (print.print && (err.errno < STARTBIT_MISSED)) {
if (err.errno > BAD_DATA_PARITY) if (err.errno > BAD_DATA_PARITY)
in->length = 0; in->length = 0;
in->print(print); in->print(print);
} }
return err.errno; if (err.errno != Read{0})
return unexpected(err.errno);
return in;
} }
auto Bus::send(const Frame *out, Frame::Print print) -> Send { auto Bus::send(const Frame &out, Frame::Print print) -> Send {
struct errtype { struct errtype {
// Error enum is ordered such that a lower numeric value corresponds to // Error enum is ordered such that a lower numeric value corresponds to
// more success // more success
@@ -334,35 +351,35 @@ auto Bus::send(const Frame *out, Frame::Print print) -> Send {
goto handle_err; goto handle_err;
} }
handle.send<1>(static_cast<uint8_t>(out->is_unicast), no_parity); handle.send<1>(static_cast<uint8_t>(out.is_unicast), no_parity);
handle.send<12>(out->controller_addr, with_parity); handle.send<12>(out.controller_addr, with_parity);
if (auto serr = if (auto serr =
handle.send<12>(out->peripheral_addr, with_ack, out->is_unicast); handle.send<12>(out.peripheral_addr, with_ack, out.is_unicast);
serr == NAK) { serr == NAK) {
err.errno = NAK_ADDRESS; err.errno = NAK_ADDRESS;
goto handle_err; goto handle_err;
} }
if (auto serr = handle.send<4>(out->control, with_ack, out->is_unicast); if (auto serr = handle.send<4>(out.control, with_ack, out.is_unicast);
serr == NAK) { serr == NAK) {
err.errno = NAK_CONTROL; err.errno = NAK_CONTROL;
goto handle_err; goto handle_err;
} }
if (auto serr = handle.send<8>(out->length, with_ack, out->is_unicast); if (auto serr = handle.send<8>(out.length, with_ack, out.is_unicast);
serr == NAK) { serr == NAK) {
err.errno = NAK_MESSAGE_LENGTH; err.errno = NAK_MESSAGE_LENGTH;
goto handle_err; goto handle_err;
} }
for (uint8_t i = 0; i < out->length; i++) { for (uint8_t i = 0; i < out.length; i++) {
// Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem // Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem
// necessary (i.e. This deviates from the previous broadcast specific // necessary (i.e. This deviates from the previous broadcast specific
// function that sent an extra `1` bit after each byte/parity) // function that sent an extra `1` bit after each byte/parity)
// Explanation for why audio-group broadcast state report isn't working? // Explanation for why audio-group broadcast state report isn't working?
if (auto serr = handle.send<8>(out->data[i], with_ack, out->is_unicast); if (auto serr = handle.send<8>(out.data[i], with_ack, out.is_unicast);
serr == NAK) { serr == NAK) {
err.errno = NAK_DATA; err.errno = NAK_DATA;
err.val = i; err.val = i;
@@ -399,14 +416,14 @@ auto Bus::send(const Frame *out, Frame::Print print) -> Send {
} }
if (print.print) if (print.print)
out->print(print); out.print(print);
return err.errno; return err.errno;
} }
Bus::Handle Bus::get() { return Handle{*this}; }; Bus::Handle Bus::get() { return Handle{*this}; };
#ifndef NDEBUG #if !defined(NDEBUG) && defined(MEASURE_BUS)
// Debug bit-timing measurement on the one physical bus; instance-scoped for the // Debug bit-timing measurement on the one physical bus; instance-scoped for the
// same reason as is_active(). // same reason as is_active().
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
+5 -2
View File
@@ -48,9 +48,11 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
namespace avclan { namespace avclan {
@@ -77,8 +79,9 @@ public:
void measure(); void measure();
#endif #endif
Error::Read read(uint16_t address, Frame *in, Frame::Print print); expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address,
Error::Send send(const Frame *out, Frame::Print print); Frame::Print print);
Error::Send send(const Frame &out, Frame::Print print);
private: private:
class Handle; class Handle;
+89 -75
View File
@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
@@ -56,17 +57,17 @@ void CDChanger::init() {
cdtimer_init(this, &incrementTime_callback, &isPlaying_callback); cdtimer_init(this, &incrementTime_callback, &isPlaying_callback);
} }
void CDChanger::handle(const Frame *in, Frame *out) { void CDChanger::handle(const Frame &in, Frame &out) {
if (in->length < 4) if (in.length < 4)
return; // [Currently known] valid CDChanger frames have at least 4 bytes return; // [Currently known] valid CDChanger frames have at least 4 bytes
const uint8_t *data = &in->data[1]; const uint8_t *data = &in.data[1];
const auto from = static_cast<Device>(*data++); const auto from = static_cast<Device>(*data++);
/* const auto to = */ data++; /* const auto to = */ data++;
const auto action = static_cast<Action>(*data++); const auto action = static_cast<Action>(*data++);
static const uint8_t function_change_resp[] = { static const uint8_t function_change_resp[] = {
0x00, to_underlying(Device::CD_CHANGER), to_underlying(from), 0xFF, 0x01}; 0x00, to_underlying(Device::CD_CHANGER), 0xFF, 0xFF, 0x01};
using enum Action; using enum Action;
#pragma GCC diagnostic push #pragma GCC diagnostic push
@@ -74,26 +75,30 @@ void CDChanger::handle(const Frame *in, Frame *out) {
// Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]).
switch (action) { switch (action) {
case Enable_Function_Req: case Enable_Function_Req:
out->is_unicast = true; out.is_unicast = true;
out->length = sizeof(function_change_resp); out.length = sizeof(function_change_resp);
memcpy(out->data, function_change_resp, sizeof(function_change_resp)); memcpy(out.data, function_change_resp, sizeof(function_change_resp));
out->data[3] = to_underlying(Enable_Function_Resp); out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Enable_Function_Resp);
state = 0; state = 0;
flags2 = 0x80; flags2 = 0x80;
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case Disable_Function_Req: case Disable_Function_Req:
// No change/response needed if we're already not playing // Head unit always expects a response, but the state change can be
// conditional
out.is_unicast = true;
out.length = sizeof(function_change_resp);
memcpy(out.data, function_change_resp, sizeof(function_change_resp));
out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Disable_Function_Resp);
if (isPlaying()) { if (isPlaying()) {
stopPlaying(); 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; state = 0;
flags2 = 0x80; flags2 = 0x80;
out->is_unicast = true; out.reaction = r_StatusReport;
out->reaction = r_StatusReport; } else
} out.reaction = r_SendOnly;
break; break;
case Eject: { case Eject: {
// "Eject" label is multiply wrong; proper meaning unclear: // "Eject" label is multiply wrong; proper meaning unclear:
@@ -106,20 +111,20 @@ void CDChanger::handle(const Frame *in, Frame *out) {
if (static_cast<bool>(state & SEEKING)) { // FF/RW button released if (static_cast<bool>(state & SEEKING)) { // FF/RW button released
state &= ~SEEKING; state &= ~SEEKING;
} else { } else {
out->is_unicast = true; out.is_unicast = true;
{ {
const uint8_t msg[] = {0x00, to_underlying(Device::CD_CHANGER), const uint8_t msg[] = {0x00, to_underlying(Device::CD_CHANGER),
to_underlying(Device::CMD_SW), to_underlying(Device::CMD_SW),
to_underlying(Insertion), 0x01}; to_underlying(Insertion), 0x01};
out->length = sizeof(msg); out.length = sizeof(msg);
memcpy(out->data, msg, sizeof(msg)); memcpy(out.data, msg, sizeof(msg));
} }
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
} }
break; break;
} }
case Initial_Report_Req: { case Initial_Report_Req: {
out->is_unicast = true; out.is_unicast = true;
// No knowledge/understanding of field meaning/interpretation // No knowledge/understanding of field meaning/interpretation
const uint8_t cdinitreport_resp[] = {0x00, const uint8_t cdinitreport_resp[] = {0x00,
to_underlying(Device::CD_CHANGER), to_underlying(Device::CD_CHANGER),
@@ -130,29 +135,29 @@ void CDChanger::handle(const Frame *in, Frame *out) {
0x10, 0x10,
0x01, 0x01,
0x01}; 0x01};
out->length = sizeof(cdinitreport_resp); out.length = sizeof(cdinitreport_resp);
memcpy(out->data, cdinitreport_resp, sizeof(cdinitreport_resp)); memcpy(out.data, cdinitreport_resp, sizeof(cdinitreport_resp));
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case Playback_Req: case Playback_Req:
out->data[0] = 0x00; out.data[0] = 0x00;
out->data[1] = to_underlying(Device::CD_CHANGER); out.data[1] = to_underlying(Device::CD_CHANGER);
out->data[2] = to_underlying(from); out.data[2] = to_underlying(from);
out->data[3] = to_underlying(Playback_Resp); out.data[3] = to_underlying(Playback_Resp);
out->length = WIRE_SIZE + 4; out.length = WIRE_SIZE + 4;
serialize(&out->data[4]); serialize(&out.data[4]);
out->is_unicast = true; out.is_unicast = true;
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
case Loading_Req: case Loading_Req:
out->data[0] = 0x00; out.data[0] = 0x00;
out->length = sizeof(cdloading_resp) + 1; out.length = sizeof(cdloading_resp) + 1;
memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); memcpy(&out.data[1], cdloading_resp, sizeof(cdloading_resp));
out->data[2] = to_underlying(from); out.data[2] = to_underlying(from);
out->data[3] = to_underlying(Loading_Resp); out.data[3] = to_underlying(Loading_Resp);
out->is_unicast = true; out.is_unicast = true;
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
case Track_Seek_Up: case Track_Seek_Up:
state = SEEKING_TRACK; state = SEEKING_TRACK;
@@ -165,7 +170,7 @@ void CDChanger::handle(const Frame *in, Frame *out) {
flags2 = 0xc0; flags2 = 0xc0;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Track_Next); media_action(MediaAction::Track_Next);
out->reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Seek_Down: case Track_Seek_Down:
state = SEEKING_TRACK; state = SEEKING_TRACK;
@@ -182,7 +187,7 @@ void CDChanger::handle(const Frame *in, Frame *out) {
secs = 0x7f; secs = 0x7f;
flags2 = 0xc0; flags2 = 0xc0;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Fast_Forward: { case Track_Fast_Forward: {
state |= SEEKING; state |= SEEKING;
@@ -195,7 +200,7 @@ void CDChanger::handle(const Frame *in, Frame *out) {
media_action(MediaAction::Skip_Forward); media_action(MediaAction::Skip_Forward);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now // is ~1 sec from now
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case Track_Rewind: { case Track_Rewind: {
@@ -215,66 +220,70 @@ void CDChanger::handle(const Frame *in, Frame *out) {
media_action(MediaAction::Skip_Backward); media_action(MediaAction::Skip_Backward);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now // is ~1 sec from now
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case CD_Enable_Random: case CD_Enable_Random:
flags |= RANDOM; flags |= RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Random: case CD_Disable_Random:
flags &= ~RANDOM; flags &= ~RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Repeat: case CD_Enable_Repeat:
flags |= REPEAT; flags |= REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Repeat: case CD_Disable_Repeat:
flags &= ~REPEAT; flags &= ~REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Disk_Random: case CD_Enable_Disk_Random:
flags |= DISK_RANDOM; flags |= DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Disk_Random: case CD_Disable_Disk_Random:
flags &= ~DISK_RANDOM; flags &= ~DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Disk_Repeat: case CD_Enable_Disk_Repeat:
flags |= DISK_REPEAT; flags |= DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Disk_Repeat: case CD_Disable_Disk_Repeat:
flags &= ~DISK_REPEAT; flags &= ~DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
default: break; default: break;
} }
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
void CDChanger::react(Frame *out, detail::Error::Send err) { std::unique_ptr<Frame> CDChanger::react(
auto resp = static_cast<reaction_t>(out->reaction); expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_Nothing;
switch (resp) { if (!exp) {
case r_StateReport: if (exp.error().reaction == to_underlying(r_StateReport) &&
if (err == detail::Error::Send::NAK_ADDRESS && exp.error().err == detail::Error::Send::NAK_ADDRESS &&
++failedStatusReports > 1) { ++failedStatusReports > 1) {
failedStatusReports = 0; failedStatusReports = 0;
stopPlaying(); // Disable periodic updates if e.g. no-one's stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?) // listening (car was turned off?)
} }
break; } else {
auto out = std::move(exp.value());
auto resp = static_cast<reaction_t>(out->reaction);
out->reaction = r_Nothing;
switch (resp) {
case r_Ejection: { case r_Ejection: {
const uint8_t play[] = {0x00, const uint8_t play[] = {0x00,
to_underlying(Device::COMM_CTRL), to_underlying(Device::COMM_CTRL),
@@ -303,12 +312,12 @@ void CDChanger::react(Frame *out, detail::Error::Send err) {
[[fallthrough]]; [[fallthrough]];
case r_NormalizeState: case r_NormalizeState:
normalizeState(); normalizeState();
generateStatus(out, false, Device::STATUS); generateStatus(*out, false, Device::STATUS);
out->reaction = r_SendOnly; out->reaction = r_SendOnly;
break; break;
case r_StartPlaying: case r_StartPlaying:
normalizeState(); normalizeState();
generateStatus(out, false, Device::STATUS); generateStatus(*out, false, Device::STATUS);
out->reaction = r_BeganPlaying; out->reaction = r_BeganPlaying;
break; break;
case r_BeganPlaying: case r_BeganPlaying:
@@ -316,16 +325,23 @@ void CDChanger::react(Frame *out, detail::Error::Send err) {
out->reaction = r_Nothing; out->reaction = r_Nothing;
break; break;
case r_StatusReport: case r_StatusReport:
generateStatus(out, false, Device::STATUS); generateStatus(*out, false, Device::STATUS);
out->reaction = r_SendOnly; out->reaction = r_SendOnly;
break; break;
case r_StateReport: [[fallthrough]];
case r_SendOnly: [[fallthrough]]; case r_SendOnly: [[fallthrough]];
case r_Nothing: [[fallthrough]]; case r_Nothing: [[fallthrough]];
default: out->reaction = r_Nothing; default: break;
} }
if (out->reaction > r_Nothing)
return out;
}
return {};
} }
void CDChanger::enable(Frame *out) { void CDChanger::enable(Frame &out) {
if (!isPlaying()) { if (!isPlaying()) {
if (mins > TWODIGIT_MAX) if (mins > TWODIGIT_MAX)
mins = 0; mins = 0;
@@ -334,18 +350,16 @@ void CDChanger::enable(Frame *out) {
state = SEEKING | SEEKING_TRACK; state = SEEKING | SEEKING_TRACK;
flags2 = 0xc0; flags2 = 0xc0;
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out->reaction = r_StartPlaying; out.reaction = r_StartPlaying;
} }
} }
bool CDChanger::pending() { return cdtimer_pending(); } bool CDChanger::pending() { return cdtimer_pending(); }
void CDChanger::resolvepending() { cdtimer_clear(); }
void CDChanger::emit(Frame *out, uint16_t peripheral) { void CDChanger::emit(Frame &out) {
out->owning_device = id; // so react() routes r_StateReport back here
out->peripheral_addr = peripheral;
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out->reaction = r_StateReport; out.reaction = r_StateReport;
cdtimer_clear();
} }
bool CDChanger::isPlaying() const { return playing; } bool CDChanger::isPlaying() const { return playing; }
@@ -401,15 +415,15 @@ void CDChanger::incrementTime() {
} }
// Used for changed status messages // Used for changed status messages
void CDChanger::generateStatus(Frame *status, bool is_unicast, void CDChanger::generateStatus(Frame &status, bool is_unicast,
Device to) const { Device to) const {
status->is_unicast = is_unicast; status.is_unicast = is_unicast;
if (!is_unicast) if (!is_unicast)
status->peripheral_addr = 0x1FF; status.peripheral_addr = 0x1FF;
status->control = 0xF; status.control = 0xF;
status->length = WIRE_SIZE + ((is_unicast) ? 4 : 3); status.length = WIRE_SIZE + ((is_unicast) ? 4 : 3);
uint8_t *data = status->data; uint8_t *data = status.data;
if (is_unicast) if (is_unicast)
*data++ = 0x00; *data++ = 0x00;
*data++ = to_underlying(Device::CD_CHANGER); *data++ = to_underlying(Device::CD_CHANGER);
+8 -7
View File
@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "device.hpp" #include "device.hpp"
@@ -60,13 +61,13 @@ public:
static constexpr Device id = Device::CD_CHANGER; static constexpr Device id = Device::CD_CHANGER;
void init(); void init();
void handle(const Frame *in, Frame *out); void handle(const Frame &in, Frame &out);
void react(Frame *out, detail::Error::Send err); std::unique_ptr<Frame>
void enable(Frame *out); react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
void disable(Frame *out); void enable(Frame &out);
void disable(Frame &out);
static bool pending(); static bool pending();
static void resolvepending(); void emit(Frame &out);
void emit(Frame *out, uint16_t peripheral);
void incrementTime(); void incrementTime();
bool isPlaying() const; bool isPlaying() const;
#ifndef NDEBUG #ifndef NDEBUG
@@ -80,7 +81,7 @@ private:
void stopPlaying(); void stopPlaying();
void serialize(uint8_t *dst) const; void serialize(uint8_t *dst) const;
void setTime(uint8_t mins, uint8_t secs); void setTime(uint8_t mins, uint8_t secs);
void generateStatus(Frame *status, bool is_unicast, Device to) const; void generateStatus(Frame &status, bool is_unicast, Device to) const;
void normalizeState(); void normalizeState();
bool playing = false; bool playing = false;
+10 -5
View File
@@ -5,9 +5,11 @@
#include <concepts> #include <concepts>
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
namespace avclan { namespace avclan {
@@ -48,14 +50,17 @@ enum class Device : uint8_t {
template <class T> template <class T>
concept DeviceInterface = concept DeviceInterface =
requires { std::integral_constant<Device, T::id>{}; } && requires { std::integral_constant<Device, T::id>{}; } &&
requires(T dev, const Frame *in, Frame *out, detail::Error::Send err, requires(T dev, const Frame &in, Frame &out,
uint16_t peripheral) { expected<std::unique_ptr<Frame>, detail::SendError> exp) {
dev.init(); dev.init();
dev.handle(in, out); dev.handle(in, out);
dev.enable(out); dev.enable(out);
dev.react(out, err); {
dev.react(std::move(exp))
} -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>; { dev.pending() } -> std::convertible_to<bool>;
dev.resolvepending(); // Devices must clear `pending()` after `emit()` is called
dev.emit(out, peripheral); dev.emit(out);
}; };
} // namespace avclan } // namespace avclan
+50
View File
@@ -9,12 +9,62 @@
#include "frame.hpp" #include "frame.hpp"
#if defined(AVCLAN_FRAME_POOL_N)
#include <array>
#include <cstddef>
#include <limits>
#include <new>
namespace {
template <class T, std::uint8_t N>
requires(N >= 1 && N <= std::numeric_limits<uint8_t>::max())
class Pool {
public:
constexpr Pool() {
for (uint8_t i = 0; i < N; ++i)
ptrs_[i] = &storage_[i];
}
T *acquire() {
if (top_ == 0)
return nullptr;
return ptrs_[--top_];
}
void release(T *ptr) {
// Properly would need an origin check/confirmation if this was used more
// generally
ptrs_[top_++] = ptr;
}
private:
std::array<T, N> storage_;
std::array<T *, N> ptrs_;
uint8_t top_ = N;
};
Pool<avclan::Frame, AVCLAN_FRAME_POOL_N> pool;
} // namespace
#endif
namespace { namespace {
using Error = avclan::detail::Error; using Error = avclan::detail::Error;
using enum Error::Parse; using enum Error::Parse;
} // namespace } // namespace
namespace avclan { namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept {
return pool.acquire();
}
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept {
pool.release(static_cast<Frame *>(ptr));
}
#endif
void Frame::print(Frame::Print print) const { void Frame::print(Frame::Print print) const {
if (print.binary) { if (print.binary) {
uint8_t buffer[8]; uint8_t buffer[8];
+19 -3
View File
@@ -9,6 +9,11 @@
#include "avclan.h" #include "avclan.h"
#if defined(AVCLAN_FRAME_POOL_N)
#include <cstddef>
#include <new>
#endif
namespace avclan { namespace avclan {
enum class Device : uint8_t; enum class Device : uint8_t;
@@ -25,13 +30,24 @@ struct Frame {
Error::Parse parse(const uint8_t *bytes, uint8_t len); Error::Parse parse(const uint8_t *bytes, uint8_t len);
void print(Print print) const; void print(Print print) const;
uint8_t reaction; #if defined(AVCLAN_FRAME_POOL_N)
Device owning_device; // O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported.
static void *operator new(std::size_t) = delete;
static void *operator new(std::size_t, const std::nothrow_t &) noexcept;
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
static void operator delete(void *) noexcept;
#endif
// reaction has a Device-defined interpretation, with the sole invariant that
// 0 == inactive/non-sendable frame
uint8_t reaction = 0;
Device owning_device = NoDevice;
bool is_unicast; bool is_unicast;
uint16_t controller_addr; // formerly "master" uint16_t controller_addr; // formerly "master"
uint16_t peripheral_addr; // formerly "slave" uint16_t peripheral_addr; // formerly "slave"
uint8_t control = 0xF; uint8_t control = 0xF;
uint8_t length; uint8_t length = 0;
uint8_t data[MAXLENGTH]; uint8_t data[MAXLENGTH];
}; };
} // namespace avclan } // namespace avclan
+1 -1
View File
@@ -77,7 +77,7 @@ Bit phy_read_bits_u8(uint8_t *bits, uint8_t len);
Bit phy_read_bits_u16(uint16_t *bits, int8_t len); Bit phy_read_bits_u16(uint16_t *bits, int8_t len);
Bit phy_read_byte(uint8_t *byte); Bit phy_read_byte(uint8_t *byte);
#ifndef NDEBUG #if !defined(NDEBUG) && defined(MEASURE_BUS)
// Sample and dump bus bit timing over the serial link (REPL `M`). // Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void); void phy_measure(void);
#endif #endif
+132 -49
View File
@@ -4,20 +4,41 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <tuple>
#include <utility>
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "device.hpp" #include "device.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
#include <cstdint>
#include <cstring>
#include <tuple>
namespace avclan { namespace avclan {
namespace detail {
enum class Party : uint8_t { Sender, Recipient };
}
template <DeviceInterface... Devs> class Peripheral { template <DeviceInterface... Devs> class Peripheral {
using Party = detail::Party;
using enum Party;
public: public:
using Error = detail::Error; using Error = detail::Error;
// lack of static reflection until C++26 limits our methods of doing a
// compile-time collision check. Within Peripheral, we can at least assert no
// collision with Devices *used in a particular instantiation* but it is not a
// universal check against all Device enum values
static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h");
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
bus.init(); bus.init();
(std::get<Devs>(devices_).init(), ...); (std::get<Devs>(devices_).init(), ...);
@@ -36,41 +57,56 @@ public:
Bus &get_bus() { return bus; } Bus &get_bus() { return bus; }
#endif #endif
Error::Read read(Frame *in, Frame::Print print) { expected<std::unique_ptr<Frame>, Error::Read>
return bus.read(address_, in, print); read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print);
}; };
Error::Send send(Frame *out, Frame::Print print) {
expected<std::unique_ptr<Frame>, detail::SendError>
send(std::unique_ptr<Frame> out, Frame::Print print = Frame::Print{}) {
// To "forge" a controller_addr, instantiate a new/different Peripheral // To "forge" a controller_addr, instantiate a new/different Peripheral
postmark(out); stamp<Sender>(*out);
return bus.send(out, print); out->control = 0xF;
auto err = bus.send(*out, print);
if (err != Error::Send{0})
return unexpected{
detail::SendError{out->owning_device, out->reaction, err}};
return out;
}; };
#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) #define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c))
void route(const Frame *in, Frame *out) { // expected needed to distinguish don't vs can't respond
expected<std::unique_ptr<Frame>, Error::Read> route(const Frame &in) {
using enum Device; using enum Device;
using enum Action; using enum Action;
out->reaction = 0;
if (is_muted() || in->length < 3) if (is_muted() || in.length < 3)
return; return {};
std::unique_ptr<Frame> out(new (std::nothrow) Frame);
if (!out) {
puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY};
}
// 0xFF placeholders are variant bytes filled by writing directly to // 0xFF placeholders are variant bytes filled by writing directly to
// out->data[N] after memcpy. // out->data[N] after memcpy.
static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL), static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL),
to_underlying(LAN), 0xFF, 0xFF}; to_underlying(LAN), 0xFF, 0xFF};
out->peripheral_addr = controller_; stamp<Recipient>(*out);
const uint8_t *data = in->data; const uint8_t *data = in.data;
const uint8_t b0 = *data++; const uint8_t b0 = *data++;
const uint8_t b1 = *data++; const uint8_t b1 = *data++;
const uint8_t b2 = *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) { // the shortest known/valid messages are 3 bytes long
const uint8_t b3 = (in.length > 3) ? *data++ : 0;
if (!in.is_unicast) {
const auto from = b0; const auto from = b0;
const auto to = b1; const auto to = b1;
const auto action = b2; const auto action = b2;
@@ -103,15 +139,14 @@ public:
case PACK3(COMMUNICATION_V1, COMM_CTRL, case PACK3(COMMUNICATION_V1, COMM_CTRL,
to_underlying(Advertise_Function)): to_underlying(Advertise_Function)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, case PACK3(COMMUNICATION_V2, COMM_CTRL,
to_underlying(Advertise_Function)): to_underlying(Advertise_Function)): {
auto enable_d = [](auto &d, auto &out) { d.enable(out); };
((Devs::id == static_cast<Device>(b3) ((Devs::id == static_cast<Device>(b3)
? [&] { ? originate(std::get<Devs>(devices_), *out, enable_d)
out->owning_device = Devs::id;
std::get<Devs>(devices_).enable(out);
}()
: void()), : void()),
...); ...);
break; break;
}
case PACK3(COMMUNICATION_V1, COMM_CTRL, to_underlying(Ping_Req)): case PACK3(COMMUNICATION_V1, COMM_CTRL, to_underlying(Ping_Req)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(Ping_Req)): { case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(Ping_Req)): {
out->is_unicast = true; out->is_unicast = true;
@@ -127,8 +162,8 @@ public:
to_underlying(List_Functions_Req)): to_underlying(List_Functions_Req)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, case PACK3(COMMUNICATION_V2, COMM_CTRL,
to_underlying(List_Functions_Req)): { to_underlying(List_Functions_Req)): {
controller_ = in->controller_addr; controller_ = in.controller_addr;
out->peripheral_addr = controller_; stamp<Recipient>(*out); // re-stamp now that controller_ is known
out->is_unicast = true; out->is_unicast = true;
const uint8_t list_functions_resp[] = { const uint8_t list_functions_resp[] = {
0x00, to_underlying(COMM_CTRL), from, 0x00, to_underlying(COMM_CTRL), from,
@@ -141,44 +176,92 @@ public:
// case Restart_Lan: not handled // case Restart_Lan: not handled
default: break; default: break;
} }
} else if (in->peripheral_addr == address_ && b0 == 0x00) { } else if (in.peripheral_addr == address_ && b0 == 0x00) {
auto handle_d = [&](auto &d, auto &out) { d.handle(in, out); };
((Devs::id == static_cast<Device>(b2) ((Devs::id == static_cast<Device>(b2)
? device_preroute(std::get<Devs>(devices_), in, out), ? originate(std::get<Devs>(devices_), *out, handle_d)
0 : 0), : void()),
...); ...);
} }
if (out->reaction > 0)
return out;
return {};
} }
#undef PACK3 #undef PACK3
void react(Frame *out, Error::Send err) { std::unique_ptr<Frame>
if (((Devs::id == out->owning_device) || ...)) react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
((Devs::id == out->owning_device const Device from =
? std::get<Devs>(devices_).react(out, err), exp ? exp.value()->owning_device : exp.error().owning_device;
0 : 0),
std::unique_ptr<Frame> next;
((Devs::id == from &&
(next = std::get<Devs>(devices_).react(std::move(exp)))) ||
...); ...);
else return next;
out->reaction = 0;
} }
template <class F> void poll_devices(F &&fun) { // Service ready devices in round-robin order
(poller(std::get<Devs>(devices_), fun), ...); std::unique_ptr<Frame> poll() {
using U = std::unique_ptr<Frame>;
auto does_emit = [&](DeviceInterface auto &dev) -> U {
if (!dev.pending())
return {};
U out(new (std::nothrow) Frame);
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
originate(dev, *out, [](auto &d, auto &out) { d.emit(out); });
return out;
};
// Runtime tuple index helper
auto does_index_emit = [&](std::size_t t) -> U {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
U out;
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
return out;
}(std::index_sequence_for<Devs...>{});
};
constexpr std::size_t N = sizeof...(Devs);
if constexpr (N == 1) { // round-robin not needed
return does_emit(std::get<0>(devices_));
} else {
static uint8_t rr_ = 0; // round-robin cursor
const std::size_t start = rr_;
for (std::size_t t = start; t < N; ++t) // [start, N)
if (auto out = does_index_emit(t)) {
rr_ = (t + 1 == N) ? 0 : t + 1;
return out;
}
for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N
if (auto out = does_index_emit(t)) {
rr_ = t + 1;
return out;
}
return {};
}
} }
private: private:
void postmark(Frame *out) const { template <Party P> void stamp(Frame &out) const {
out->controller_addr = address_; if constexpr (P == Sender)
out->control = 0xF; out.controller_addr = address_;
else
out.peripheral_addr = controller_;
} }
template <DeviceInterface Dev, class F> void poller(Dev &dev, F &&fun) { void originate(DeviceInterface auto &dev, Frame &out, auto &&fill) {
if (dev.pending() && fun(dev)) out.owning_device = std::remove_reference_t<decltype(dev)>::id;
dev.resolvepending(); stamp<Recipient>(out); // default set FIRST; fill() may override
} fill(dev, out);
template <DeviceInterface Dev>
void device_preroute(Dev &dev, const Frame *in, Frame *out) {
out->owning_device = Dev::id;
dev.handle(in, out);
} }
Bus &bus; Bus &bus;
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#include <tl/expected.hpp>
namespace avclan {
template <class T, class E> using expected = tl::expected<T, E>;
template <class E> using unexpected = tl::unexpected<E>;
using tl::unexpect;
using tl::unexpect_t;
} // namespace avclan
#else
#include <expected>
namespace avclan {
template <class T, class E> using expected = std::expected<T, E>;
template <class E> using unexpected = std::unexpected<E>;
using std::unexpect;
using std::unexpect_t;
} // namespace avclan
#endif
@@ -76,6 +76,9 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json. # tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)") set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_FRAME_POOL_N 24 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
try_compile(LIBC_VERSION_TEST try_compile(LIBC_VERSION_TEST
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU} COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -85,11 +88,14 @@ FetchContent_Declare(
avr_libstdcpp avr_libstdcpp
GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git
GIT_TAG 5354296040a2289c911062daa82336762231e897 GIT_TAG 5354296040a2289c911062daa82336762231e897
SYSTEM
) )
FetchContent_MakeAvailable(avr_libstdcpp) FetchContent_MakeAvailable(avr_libstdcpp)
add_library(libstdcpp INTERFACE) add_library(libstdcpp STATIC ${avr_libstdcpp_SOURCE_DIR}/src/new_handler.cc)
target_include_directories(libstdcpp SYSTEM target_include_directories(libstdcpp
INTERFACE ${avr_libstdcpp_SOURCE_DIR}/include) PUBLIC ${avr_libstdcpp_SOURCE_DIR}/include)
# Let --gc-sections drop the unused set/get_new_handler functions.
target_compile_options(libstdcpp PRIVATE -ffunction-sections -fdata-sections)
target_link_libraries(avclan PUBLIC libstdcpp) target_link_libraries(avclan PUBLIC libstdcpp)
if(NOT LIBC_VERSION_TEST) if(NOT LIBC_VERSION_TEST)
@@ -142,6 +148,7 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL} TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
) )
target_compile_options(avclan PUBLIC target_compile_options(avclan PUBLIC
--param=min-pagesize=0 --param=min-pagesize=0
+31 -7
View File
@@ -344,6 +344,14 @@ void phy_init() {
// error reporting; no printing happens here. // error reporting; no printing happens here.
Read phy_read_startbit() { Read phy_read_startbit() {
uint16_t startbitlen = TCB1.CNT = 0; uint16_t startbitlen = TCB1.CNT = 0;
// Reset the ~atomic `pulsewidth` variable to detect the post-pulse update
// from the TCB0_INT_vect ISR
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if (!BUS_IS_IDLE) // Only reset if bus is actively driven (i.e. current
pulsewidth = 0; // value is stale/already been used)
}
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
startbitlen = TCB1.CNT; startbitlen = TCB1.CNT;
if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) { if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
@@ -367,18 +375,34 @@ Read phy_read_startbit() {
return result; return result;
} }
} }
// `pulsewidth` updates once the TCB0_INT_vect ISR runs for this pulse.
TCB1.CNT = 0;
do {
if (TCB1.CNT > (uint16_t)AVCLAN_BIT0_LOGIC_1) // Wait a max of ~6μs for ISR
return BAD_STARTBIT; // ISR/other implementation bug; abort
// Read ~atomically, to prevent torn reads
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { startbitlen = pulsewidth; }
} while (startbitlen == 0);
if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) { if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) {
// We missed the beginning of this message; wait for it to finish (bus // Not a start bit; wait for the message to finish (bus continuously idle
// continuously idle for >1 bit length) before returning, so we don't have // for >1 bit length) before returning, so we only report one error (instead
// multiple false-starts while the in-progress message keeps sending more // of e.g. repeated "bad (short) start bit" errors)
// bits.
TCB1.CNT = 0; TCB1.CNT = 0;
while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) { while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) {
if (!BUS_IS_IDLE) if (!BUS_IS_IDLE)
TCB1.CNT = 0; TCB1.CNT = 0; // Reset counter after each bit pulse
} }
return STARTBIT_TOO_SHORT; // A pulse no wider than a normal bit means we merely tuned in mid-frame and
// this was a data bit; a wider-but-still-sub-start pulse means some other
// device emitted a wonky pulse.
return (startbitlen < (uint16_t)AVCLAN_BIT_LENGTH_MAX) ? STARTBIT_MISSED
: STARTBIT_MALFORMED;
} }
if (startbitlen > (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 1.2))
return STARTBIT_TOO_LONG;
return (Read)0; // that was a start bit return (Read)0; // that was a start bit
} }
@@ -432,7 +456,7 @@ void phy_guard_leave() {
} }
} }
#ifndef NDEBUG #if !defined(NDEBUG) && defined(MEASURE_BUS)
// Only used immediately below // Only used immediately below
#define XSTR(x) #x #define XSTR(x) #x
#define STR(x) XSTR(x) #define STR(x) XSTR(x)
+47 -24
View File
@@ -8,33 +8,44 @@
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <type_traits>
namespace detail { namespace detail {
template <class T, auto N> struct Deleter; template <class T, auto N> struct Deleter;
} }
template <class T, std::integral auto N, bool Owning = false> template <class T, std::integral auto N, bool Owning = false,
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max()) class Deleter = std::default_delete<T>>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max() &&
// push() drops the passed-in deleter and pop() fabricates a fresh
// one, so a deleter must either derive its state from the queue
// itself (the pool Deleter) or carry no state at all
(std::is_same_v<Deleter, detail::Deleter<T, N>> ||
(std::is_empty_v<Deleter> &&
std::is_nothrow_default_constructible_v<Deleter>)))
class Queue { class Queue {
using Deleter = detail::template Deleter<T, N>; friend detail::Deleter<T, N>;
friend Deleter;
public: public:
// Only full and copy-convert-from-full construction is allowed // Only empty construction is allowed for non-Owning, non-pool deleters
Queue() = delete; constexpr Queue()
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>)
: owner{nullptr} {}
Queue(const Queue &) = delete; Queue(const Queue &) = delete;
// Moving is unsupported due to being self-referential // Moving is unsupported due to being self-referential
Queue(Queue &&) = delete; Queue(Queue &&) = delete;
Queue &operator=(Queue &&) = delete; Queue &operator=(Queue &&) = delete;
// Construct from pre-defined storage
constexpr Queue(T (&items)[N]) constexpr Queue(T (&items)[N])
requires(Owning) requires(Owning) && std::same_as<Deleter, detail::Deleter<T, N>>
: owner{this}, write{N} { : owner{this}, write{N} {
for (uint8_t i = 0; i < N; ++i) for (uint8_t i = 0; i < N; ++i)
buf[i] = &items[i]; buf[i] = &items[i];
} }
constexpr Queue(Queue<T, N, true> &queue) // Construct non-Owning empty from Owning queue
constexpr Queue(Queue<T, N, true, Deleter> &queue)
requires(!Owning) requires(!Owning)
: owner{&queue} {} : owner{&queue} {}
@@ -46,14 +57,22 @@ public:
uint8_t push(std::unique_ptr<T, Deleter> x) uint8_t push(std::unique_ptr<T, Deleter> x)
requires(!Owning) requires(!Owning)
{ {
// structurally unnecessary; empty construction from same size parent if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
// guarantees // structurally unnecessary for detail::Deleter, where empty construction
// !isFull assert(!isFull()); // is only from same size parent
if (isFull())
return 1;
}
if (!x || !x.get_deleter().is_owned_by(owner)) if (!x)
return 1; return 1;
reclaim(x.release()); if constexpr (std::is_same_v<Deleter, detail::Deleter<T, N>>) {
if (!x.get_deleter().is_owned_by(owner))
return 1;
}
claim(x.release());
return 0; return 0;
} }
@@ -69,35 +88,39 @@ public:
if (isEmpty()) if (isEmpty())
return nullptr; return nullptr;
if constexpr (Owning) if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>)
return {buf[mask(read++)], {this}}; return {buf[mask(read++)], Deleter{}};
else if constexpr (Owning)
return {buf[mask(read++)], Deleter{this}};
else else
return {buf[mask(read++)], {owner}}; return {buf[mask(read++)], Deleter{owner}};
} }
private: private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); } uint8_t mask(uint8_t pos) const { return pos & (N - 1); }
void reclaim(T *x) { buf[mask(write++)] = x; } void claim(T *x) { buf[mask(write++)] = x; }
std::array<T *, N> buf = {}; std::array<T *, N> buf = {};
Queue<T, N, true> *const owner; Queue<T, N, true, Deleter> *const owner;
uint8_t read = 0; uint8_t read = 0;
uint8_t write = 0; uint8_t write = 0;
}; };
template <class T, auto N> Queue(Queue<T, N, true> &) -> Queue<T, N, false>; template <class T, auto N, class D>
template <class T, auto N> Queue(T (&items)[N]) -> Queue<T, N, true>; Queue(Queue<T, N, true, D> &) -> Queue<T, N, false, D>;
template <class T, auto N>
Queue(T (&items)[N]) -> Queue<T, N, true, detail::Deleter<T, N>>;
namespace detail { namespace detail {
template <class T, auto N> struct Deleter { template <class T, auto N> struct Deleter {
Deleter() = default; Deleter() = default;
constexpr Deleter(Queue<T, N, true> *owner) : owner{owner} {} constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
void operator()(T *x) const { owner->reclaim(x); } void operator()(T *x) const { owner->claim(x); }
constexpr bool is_owned_by(const Queue<T, N, true> *parent) const { constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
return owner == parent; return owner == parent;
} }
private: private:
Queue<T, N, true> *const owner = nullptr; Queue<T, N, true, Deleter> *const owner = nullptr;
}; };
} // namespace detail } // namespace detail
+62 -49
View File
@@ -7,6 +7,8 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <new>
#include <utility>
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "frame.hpp" #include "frame.hpp"
@@ -14,6 +16,7 @@
#include "hal/stdio.h" #include "hal/stdio.h"
#include "peripheral.hpp" #include "peripheral.hpp"
#include "queue.hpp" #include "queue.hpp"
#include "stdshim.hpp"
const char *const offon[] = {"OFF", "ON"}; const char *const offon[] = {"OFF", "ON"};
@@ -22,11 +25,8 @@ constexpr uint8_t CACHE_SIZE = 32;
using namespace avclan; using namespace avclan;
namespace { namespace {
Frame frames[CACHE_SIZE]; constinit Queue<Frame, CACHE_SIZE> incoming;
constinit Queue<Frame, CACHE_SIZE> outgoing;
constinit Queue cache(frames);
constinit Queue incoming = cache;
constinit Queue outgoing = cache;
uint8_t hexChars[2]; uint8_t hexChars[2];
uint8_t hexDigit = 0; // current digit being written to hexChars uint8_t hexDigit = 0; // current digit being written to hexChars
@@ -66,7 +66,6 @@ int main() {
using enum Action; using enum Action;
using enum Device; using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360); Peripheral<CDChanger> peripheral(phy, 0x360);
using Error = decltype(peripheral)::Error;
using Print = Frame::Print; using Print = Frame::Print;
Setup(); Setup();
@@ -74,44 +73,31 @@ int main() {
while (true) { while (true) {
if (peripheral.bus_is_active()) { if (peripheral.bus_is_active()) {
if (auto msg = cache.pop()) { if (auto msg = peripheral.read(Print{.print = printAllFrames,
auto err = peripheral.read(msg.get(), Print{.print = printAllFrames,
.binary = printBinary, .binary = printBinary,
.verbose = verbose}); .verbose = verbose}))
if (err == Error::Read{0x00}) incoming.push(std::move(*msg));
incoming.push(std::move(msg));
} else {
puts("!! Dropping an incoming message; cache is empty !!");
}
} }
if (const auto *in = incoming.peek()) { if (const auto *in = incoming.peek()) {
if (auto out = cache.pop()) { if (auto resp = peripheral.route(*in)) {
peripheral.route(in, out.get());
incoming.pop(); incoming.pop();
if (*resp) {
if (out->reaction > 0) outgoing.push(std::move(*resp));
outgoing.push(std::move(out)); continue; // route can be long; re-check the bus before poll/send
} else { }
puts("!! Unable to respond; cache is empty !!");
} }
} }
peripheral.poll_devices([&](auto &dev) { if (auto msg = peripheral.poll())
if (auto status = cache.pop()) { outgoing.push(std::move(msg));
dev.emit(status.get(), peripheral.controller());
outgoing.push(std::move(status));
return true;
}
return false;
});
if (auto out = outgoing.pop()) { if (auto out = outgoing.pop()) {
auto err = peripheral.send( auto result =
out.get(), Print{.print = printAllFrames, .binary = printBinary}); peripheral.send(std::move(out), Print{.print = printAllFrames,
peripheral.react(out.get(), err); .binary = printBinary});
if (out->reaction > 0) if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(out)); outgoing.push(std::move(next));
} }
// stdin must be non-blocking: yielding EOF when idle/empty // stdin must be non-blocking: yielding EOF when idle/empty
@@ -133,7 +119,7 @@ int main() {
case 'x': set_flag(&printBinary, false, "Binary:"); break; case 'x': set_flag(&printBinary, false, "Binary:"); break;
case 'E': // Beep case 'E': // Beep
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -145,10 +131,10 @@ int main() {
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} else } else
puts("!! Cache empty; unable to queue beep request"); puts("!! failed Frame alloc for Beep !! ");
break; break;
case 'P': case 'P':
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -163,7 +149,8 @@ int main() {
} }
out->reaction = CDChanger::reaction_t::r_Ejection; out->reaction = CDChanger::reaction_t::r_Ejection;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} } else
puts("!! failed Frame alloc for Play !! ");
break; break;
#ifndef NDEBUG #ifndef NDEBUG
@@ -189,7 +176,9 @@ int main() {
while (peripheral.device<CDChanger>().media_busy()) {} while (peripheral.device<CDChanger>().media_busy()) {}
puts("end"); puts("end");
break; break;
#ifdef MEASURE_BUS
case 'M': peripheral.get_bus().measure(); break; case 'M': peripheral.get_bus().measure(); break;
#endif
#endif #endif
case 0x10: // Signals binary sequence incoming case 0x10: // Signals binary sequence incoming
@@ -222,34 +211,58 @@ int main() {
if (readSeq && seqIdx > 0) { if (readSeq && seqIdx > 0) {
if (readBinary) { if (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) { if (data_tmp[seqIdx - 1] == 0x17) {
if (auto out = cache.pop()) { if (auto out =
std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (out->parse(data_tmp, --seqIdx) == if (out->parse(data_tmp, --seqIdx) ==
Frame::Error::Parse{0}) { Frame::Error::Parse{0}) {
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
}
}
readSeq = readBinary = false; readSeq = readBinary = false;
}
} else
puts("!! failed Frame alloc for input message !!");
} else } else
goto DEFAULT; // reading binary and this is a real data byte; goto DEFAULT; // reading binary and this is a real data byte;
// fall through to default // fall through to default
} else if (seqIdx <= Frame::MAXLENGTH) { } else { // ASCII message
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
const uint8_t sendLen =
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
out->is_unicast = seqIsUnicast; out->is_unicast = seqIsUnicast;
out->peripheral_addr = out->peripheral_addr =
seqIsUnicast ? peripheral.controller() : 0x1FF; seqIsUnicast ? peripheral.controller() : 0x1FF;
out->length = seqIdx; out->length = sendLen;
memcpy(out->data, data_tmp, seqIdx); memcpy(out->data, data_tmp, sendLen);
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
}
if (seqIdx > Frame::MAXLENGTH)
printf("!! sequence too long (%u > %u), truncated !!\n",
static_cast<unsigned>(seqIdx),
static_cast<unsigned>(Frame::MAXLENGTH));
// Only leave hex-entry mode and restore logging once the
// message actually sent, so a failed alloc can be retried
// with '\n' instead of silently dropping the entry.
readSeq = false;
seqIdx = hexDigit = 0;
printAllFrames = lastPrintAllFrames; printAllFrames = lastPrintAllFrames;
} else
puts("!! failed Frame alloc for input message !!");
} }
break; break;
} }
DEFAULT: DEFAULT:
default: default:
if (readSeq && seqIdx < (Frame::MAXLENGTH + sizeof(Frame))) { if (readSeq) {
// Binary mode carries the full wire preamble; hex mode is payload
// only, so it stops one past MAXLENGTH to let '\n' report overflow.
if (seqIdx >= (readBinary ? sizeof(data_tmp)
: uint8_t{Frame::MAXLENGTH + 1})) {
puts("!! sequence buffer full, ignoring further input !!");
break;
}
if (readBinary) { if (readBinary) {
data_tmp[seqIdx++] = readkey; data_tmp[seqIdx++] = readkey;
} else { } else {
@@ -291,8 +304,8 @@ void Setup() {
void print_help() { void print_help() {
puts("AVCLAN Mockingboard v1"); puts("AVCLAN Mockingboard v1");
puts("U - begin reading for unicast message\n" puts("U - begin reading for unicast message (send with Enter key)\n"
"B - begin reading for broadcast message\n" "B - begin reading for broadcast message (send with Enter key)\n"
"m - Toggle mute for mockingboard bus activity\n" "m - Toggle mute for mockingboard bus activity\n"
"v - Toggle verbose error logging\n" "v - Toggle verbose error logging\n"
"l - Toggle message logging\n" "l - Toggle message logging\n"