|
@@ -0,0 +1,445 @@
|
|
|
|
|
+#include "command_protocol.h"
|
|
|
|
|
+
|
|
|
|
|
+#include "diagnostic_queue.h"
|
|
|
|
|
+#include "hardware_seam.h"
|
|
|
|
|
+#include "mains_events.h"
|
|
|
|
|
+#include "module_provisioning.h"
|
|
|
|
|
+#include "numeric_conversion.h"
|
|
|
|
|
+#include "program_memory.h"
|
|
|
|
|
+#include "serial_output.h"
|
|
|
|
|
+#include "telemetry_reports.h"
|
|
|
|
|
+
|
|
|
|
|
+#if defined(MECOFFEE_RUST_COMMAND_READER)
|
|
|
|
|
+extern "C" uint16_t mecoffee_rust_accept_command_byte(uint8_t* bytes, uint8_t length, uint8_t receivedByte);
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+namespace meCoffee {
|
|
|
|
|
+
|
|
|
|
|
+#if defined(MECOFFEE_RUST_COMMAND_HANDLERS)
|
|
|
|
|
+extern "C" void mecoffee_rust_dispatch_command(void*, const uint8_t*, uint8_t);
|
|
|
|
|
+void dispatchCommandLine(FirmwareState* state, char* text, uint8_t length) {
|
|
|
|
|
+ mecoffee_rust_dispatch_command(state, reinterpret_cast<const uint8_t*>(text), length);
|
|
|
|
|
+}
|
|
|
|
|
+#else
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+constexpr uint8_t kMaximumArgumentCount = 4;
|
|
|
|
|
+constexpr uint8_t kCommandPrefixLength = 4;
|
|
|
|
|
+constexpr uint8_t kBoilerLeafRoutingDigitLimit = kOutputLeafCount;
|
|
|
|
|
+
|
|
|
|
|
+struct CommandArguments {
|
|
|
|
|
+ const char* values[kMaximumArgumentCount];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const char* argumentOrEmpty(const CommandArguments& arguments, uint8_t argumentIndex) {
|
|
|
|
|
+ const char* argument = arguments.values[argumentIndex];
|
|
|
|
|
+ return argument != nullptr ? argument : "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool areTextsEqual(const char* first, const char* second) {
|
|
|
|
|
+ while (*first != '\0' && *first == *second) {
|
|
|
|
|
+ ++first;
|
|
|
|
|
+ ++second;
|
|
|
|
|
+ }
|
|
|
|
|
+ return *first == *second;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool startsWithBytes(const char* text, const char* expectedPrefix, uint8_t prefixLength) {
|
|
|
|
|
+ for (uint8_t position = 0; position < prefixLength; ++position) {
|
|
|
|
|
+ if (text[position] != expectedPrefix[position]) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (expectedPrefix[position] == '\0') {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool isProgramMemoryTextEqual(const char* ramText, const char* programMemoryText) {
|
|
|
|
|
+ for (;; ++ramText, ++programMemoryText) {
|
|
|
|
|
+ const uint8_t expected = hardware::readProgramMemoryByte(programMemoryText);
|
|
|
|
|
+ if (static_cast<uint8_t>(*ramText) != expected) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (expected == 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void writeSettingValueText(const FirmwareState& state, uint8_t settingIndex) {
|
|
|
|
|
+ writeSignedDecimalText(readSettingForDisplay(state.settings, settingIndex));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void reportDumpFinished(FirmwareState& state) {
|
|
|
|
|
+ (void)state;
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd dump "));
|
|
|
|
|
+ writeUnsignedDecimalText(kSettingCount);
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" OK"));
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void reportNextDumpedSetting(FirmwareState& state) {
|
|
|
|
|
+ const uint8_t settingIndex = state.diagnosticQueue.dumpCursor;
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd get "));
|
|
|
|
|
+ writeProgramMemoryText(settingDescriptorKeyAddress(settingIndex));
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeSettingValueText(state, settingIndex);
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" OK"));
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+
|
|
|
|
|
+ state.diagnosticQueue.dumpCursor = static_cast<uint8_t>(settingIndex + 1);
|
|
|
|
|
+ if (state.diagnosticQueue.dumpCursor == kSettingCount) {
|
|
|
|
|
+ state.diagnosticQueue.dumpCursor = 0;
|
|
|
|
|
+ enqueueDiagnosticCallback(&state.diagnosticQueue, reportDumpFinished);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ enqueueDiagnosticCallback(&state.diagnosticQueue, reportNextDumpedSetting);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void reportStoredOscillatorCalibration(FirmwareState& state) {
|
|
|
|
|
+ (void)state;
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd osccal = "));
|
|
|
|
|
+ writeSignedDecimalText(hardware::readPersistentByte(0));
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" OK"));
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleUname(FirmwareState*, const CommandArguments&) {
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("meCoffee pcb V9"));
|
|
|
|
|
+ for (uint8_t signatureByteIndex = 0; signatureByteIndex < 6; signatureByteIndex += 2) {
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeUnsignedDecimalText(hardware::readDeviceSignatureByte(signatureByteIndex));
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleDump(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ enqueueDiagnosticCallback(&state->diagnosticQueue, reportNextDumpedSetting);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleOscillatorCalibration(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ hardware::writePersistentByte(0, hardware::readOscillatorCalibrationRegister());
|
|
|
|
|
+ enqueueDiagnosticCallback(&state->diagnosticQueue, reportStoredOscillatorCalibration);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleBoilerOutput(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ OutputGroup& boiler = groupForRole(&state->outputChannels, OutputGroupRole::Boiler);
|
|
|
|
|
+ const char* argument = argumentOrEmpty(arguments, 0);
|
|
|
|
|
+ if (areTextsEqual(argument, "on")) {
|
|
|
|
|
+ setGroupEnabled(&boiler, true);
|
|
|
|
|
+ } else if (areTextsEqual(argument, "off")) {
|
|
|
|
|
+ setGroupEnabled(&boiler, false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setGroupAmplitude(&boiler, parseIntegerPrefixModulo65536(argument));
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleAlternateMode(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ state->alternateModeSelection = static_cast<uint8_t>(parseIntegerPrefixModulo65536(argumentOrEmpty(arguments, 0)));
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleSetting(FirmwareState* state, const CommandArguments& arguments);
|
|
|
|
|
+bool handleSettingQuery(FirmwareState* state, const CommandArguments& arguments);
|
|
|
|
|
+
|
|
|
|
|
+bool handleResetPin(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ constexpr uint8_t kBootloaderResetLogicalPin = 16;
|
|
|
|
|
+ const char* argument = argumentOrEmpty(arguments, 0);
|
|
|
|
|
+ hardware::configureLogicalPin(kBootloaderResetLogicalPin, hardware::PinConfiguration::Output);
|
|
|
|
|
+ if (argument[0] == 'e' && argument[1] == 'n') {
|
|
|
|
|
+ hardware::writeLogicalPin(kBootloaderResetLogicalPin, false);
|
|
|
|
|
+ hardware::configureLogicalPin(kBootloaderResetLogicalPin, hardware::PinConfiguration::Input);
|
|
|
|
|
+ state->diagnosticQueue.isReportingInhibited = 1;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hardware::writeLogicalPin(kBootloaderResetLogicalPin, true);
|
|
|
|
|
+ state->diagnosticQueue.isReportingInhibited = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleFilteredPhaseReport(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ writeUnsignedDecimalText(state->mainsPhase.filteredTicksPerHalfCycle);
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handlePump(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ const uint8_t amplitudeByte = static_cast<uint8_t>(parseIntegerPrefixModulo65536(argumentOrEmpty(arguments, 0)));
|
|
|
|
|
+ OutputGroup& pump = groupForRole(&state->outputChannels, OutputGroupRole::Pump);
|
|
|
|
|
+ setGroupAmplitude(&pump, amplitudeByte);
|
|
|
|
|
+ setGroupEnabled(&pump, amplitudeByte != 255);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleClock(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ const char* subcommand = argumentOrEmpty(arguments, 0);
|
|
|
|
|
+ const char* secondArgument = argumentOrEmpty(arguments, 1);
|
|
|
|
|
+ if (startsWithBytes(subcommand, "set", 3)) {
|
|
|
|
|
+ state->eventClock.pendingSeconds = parseIntegerPrefixModulo4294967296(secondArgument);
|
|
|
|
|
+ state->eventClock.isValid = 1;
|
|
|
|
|
+ } else if (startsWithBytes(subcommand, "get", 3)) {
|
|
|
|
|
+ writeUnsignedDecimalText(readEventClockSecondsAtomically(*state));
|
|
|
|
|
+ } else if (startsWithBytes(subcommand, "tmr", 3)) {
|
|
|
|
|
+ state->eventClock.timingWord = static_cast<int16_t>(parseIntegerPrefixModulo65536(secondArgument));
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleIntegralDebug(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ const int32_t setpoint = state->settings.brewSetpoint;
|
|
|
|
|
+ const int32_t measurement = state->sensor.firstChannelAggregate;
|
|
|
|
|
+ const uint32_t scaledDifference = static_cast<uint32_t>(setpoint - measurement) * 2500u;
|
|
|
|
|
+ const uint32_t divisor = static_cast<uint32_t>(setpoint);
|
|
|
|
|
+ const uint32_t quotient = divisor == 0 ? 0xffffffffUL : scaledDifference / divisor;
|
|
|
|
|
+ const uint32_t accumulator = (quotient + 1500u) * 5u;
|
|
|
|
|
+ state->heater.integralAccumulator = static_cast<int32_t>(accumulator);
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("dbg "));
|
|
|
|
|
+ writeUnsignedDecimalText(state->sensor.aggregateGeneration);
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeSignedDecimalText(state->heater.integralAccumulator);
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleCool(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ OutputGroup& valve = groupForRole(&state->outputChannels, OutputGroupRole::Valve);
|
|
|
|
|
+ OutputGroup& pump = groupForRole(&state->outputChannels, OutputGroupRole::Pump);
|
|
|
|
|
+ setGroupEnabled(&valve, true);
|
|
|
|
|
+ setGroupEnabled(&pump, true);
|
|
|
|
|
+ hardware::blockForMilliseconds(2000);
|
|
|
|
|
+ setGroupEnabled(&valve, false);
|
|
|
|
|
+ setGroupEnabled(&pump, false);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleSensorRing(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ for (int8_t ringPosition = kSensorRingLength - 1; ringPosition >= 0; --ringPosition) {
|
|
|
|
|
+ writeUnsignedDecimalText(state->sensor.firstRing[ringPosition]);
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handlePhasePolynomialReport(FirmwareState* state, const CommandArguments&) {
|
|
|
|
|
+ const uint32_t scaledPhase = static_cast<uint32_t>(state->mainsPhase.filteredTicksPerHalfCycle) * 195u;
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("poly "));
|
|
|
|
|
+ writeUnsignedDecimalText(state->sensor.aggregateGeneration);
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeUnsignedDecimalText(static_cast<uint16_t>(scaledPhase / 1608u));
|
|
|
|
|
+ writeCarriageReturnLineFeed();
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleSettingQuery(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ const char* key = argumentOrEmpty(arguments, 0);
|
|
|
|
|
+ if (startsWithBytes(key, "btname", 6)) {
|
|
|
|
|
+ if (state->module.isPresent) {
|
|
|
|
|
+ writeRamText(state->module.capturedReply);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ const int8_t settingIndex = findSettingIndexByProtocolKey(key);
|
|
|
|
|
+ if (settingIndex < 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ writeProgramMemoryText(settingDescriptorKeyAddress(static_cast<uint8_t>(settingIndex)));
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeSettingValueText(*state, static_cast<uint8_t>(settingIndex));
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool handleSetting(FirmwareState* state, const CommandArguments& arguments) {
|
|
|
|
|
+ const char* key = argumentOrEmpty(arguments, 0);
|
|
|
|
|
+ const char* valueText = argumentOrEmpty(arguments, 1);
|
|
|
|
|
+ if (startsWithBytes(key, "btname", 6)) {
|
|
|
|
|
+ if (state->module.isPresent) {
|
|
|
|
|
+ captureRadioModuleReplyInAtMode(state);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (key[0] == 'o' && key[1] >= '0' && key[1] <= '9') {
|
|
|
|
|
+ const uint8_t leafIndex = static_cast<uint8_t>(key[1] - '0');
|
|
|
|
|
+ if (leafIndex < kBoilerLeafRoutingDigitLimit) {
|
|
|
|
|
+ routeLeafToRoleCharacter(&state->outputChannels, leafIndex,
|
|
|
|
|
+ static_cast<uint8_t>(parseIntegerPrefixModulo65536(valueText)));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const int8_t settingIndex = findSettingIndexByProtocolKey(key);
|
|
|
|
|
+ if (settingIndex < 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ SettingDescriptor descriptor;
|
|
|
|
|
+ readSettingDescriptor(static_cast<uint8_t>(settingIndex), &descriptor);
|
|
|
|
|
+ const float parsedValue = parseDecimalFloatingPoint(valueText, nullptr);
|
|
|
|
|
+ uint8_t* storage = settingStorageAddress(&state->settings, static_cast<uint8_t>(settingIndex));
|
|
|
|
|
+ const uint32_t storedValue = (descriptor.byteWidth == 2 && descriptor.isSigned)
|
|
|
|
|
+ ? static_cast<uint32_t>(convertFloatToSigned32(parsedValue))
|
|
|
|
|
+ : convertFloatToUnsigned32(parsedValue, nullptr);
|
|
|
|
|
+ for (uint8_t byteIndex = 0; byteIndex < descriptor.byteWidth; ++byteIndex) {
|
|
|
|
|
+ storage[byteIndex] = static_cast<uint8_t>(storedValue >> (8 * byteIndex));
|
|
|
|
|
+ }
|
|
|
|
|
+ persistSetting(&state->settings, state->settingRecordHandles, static_cast<uint8_t>(settingIndex));
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("s_"));
|
|
|
|
|
+ writeProgramMemoryText(settingDescriptorKeyAddress(static_cast<uint8_t>(settingIndex)));
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ writeSettingValueText(*state, static_cast<uint8_t>(settingIndex));
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+using CommandHandler = bool (*)(FirmwareState*, const CommandArguments&);
|
|
|
|
|
+
|
|
|
|
|
+struct CommandEntry {
|
|
|
|
|
+ const char* name;
|
|
|
|
|
+ CommandHandler handler;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// The registered names, in the original table order. Names live in program memory.
|
|
|
|
|
+const CommandEntry* commandEntryAt(uint8_t entryIndex, CommandEntry* storage) {
|
|
|
|
|
+ static const char kUname[] MECOFFEE_PROGRAM_MEMORY = "uname";
|
|
|
|
|
+ static const char kDump[] MECOFFEE_PROGRAM_MEMORY = "dump";
|
|
|
|
|
+ static const char kOscillatorCalibration[] MECOFFEE_PROGRAM_MEMORY = "osccal";
|
|
|
|
|
+ static const char kBoiler[] MECOFFEE_PROGRAM_MEMORY = "blr";
|
|
|
|
|
+ static const char kAlternateMode[] MECOFFEE_PROGRAM_MEMORY = "stm";
|
|
|
|
|
+ static const char kSet[] MECOFFEE_PROGRAM_MEMORY = "set";
|
|
|
|
|
+ static const char kGet[] MECOFFEE_PROGRAM_MEMORY = "get";
|
|
|
|
|
+ static const char kReset[] MECOFFEE_PROGRAM_MEMORY = "reset";
|
|
|
|
|
+ static const char kMaximumInterval[] MECOFFEE_PROGRAM_MEMORY = "maxint";
|
|
|
|
|
+ static const char kPump[] MECOFFEE_PROGRAM_MEMORY = "pump";
|
|
|
|
|
+ static const char kClock[] MECOFFEE_PROGRAM_MEMORY = "clock";
|
|
|
|
|
+ static const char kIntegralTerm[] MECOFFEE_PROGRAM_MEMORY = "iterm";
|
|
|
|
|
+ static const char kCool[] MECOFFEE_PROGRAM_MEMORY = "cool";
|
|
|
|
|
+ static const char kSensor[] MECOFFEE_PROGRAM_MEMORY = "sensor";
|
|
|
|
|
+ static const char kPolynomial[] MECOFFEE_PROGRAM_MEMORY = "poly";
|
|
|
|
|
+ switch (entryIndex) {
|
|
|
|
|
+ case 0: *storage = {kUname, handleUname}; break;
|
|
|
|
|
+ case 1: *storage = {kDump, handleDump}; break;
|
|
|
|
|
+ case 2: *storage = {kOscillatorCalibration, handleOscillatorCalibration}; break;
|
|
|
|
|
+ case 3: *storage = {kBoiler, handleBoilerOutput}; break;
|
|
|
|
|
+ case 4: *storage = {kAlternateMode, handleAlternateMode}; break;
|
|
|
|
|
+ case 5: *storage = {kSet, handleSetting}; break;
|
|
|
|
|
+ case 6: *storage = {kGet, handleSettingQuery}; break;
|
|
|
|
|
+ case 7: *storage = {kReset, handleResetPin}; break;
|
|
|
|
|
+ case 8: *storage = {kMaximumInterval, handleFilteredPhaseReport}; break;
|
|
|
|
|
+ case 9: *storage = {kPump, handlePump}; break;
|
|
|
|
|
+ case 10: *storage = {kClock, handleClock}; break;
|
|
|
|
|
+ case 11: *storage = {kIntegralTerm, handleIntegralDebug}; break;
|
|
|
|
|
+ case 12: *storage = {kCool, handleCool}; break;
|
|
|
|
|
+ case 13: *storage = {kSensor, handleSensorRing}; break;
|
|
|
|
|
+ default: *storage = {kPolynomial, handlePhasePolynomialReport}; break;
|
|
|
|
|
+ }
|
|
|
|
|
+ return storage;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+constexpr uint8_t kRegisteredCommandCount = 15;
|
|
|
|
|
+
|
|
|
|
|
+} // namespace
|
|
|
|
|
+
|
|
|
|
|
+void dispatchCommandLine(FirmwareState* state, char* commandLineText, uint8_t commandLineLength) {
|
|
|
|
|
+ if (commandLineLength < kCommandPrefixLength || !startsWithBytes(commandLineText, "cmd ", kCommandPrefixLength)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ commandLineText[commandLineLength] = '\0';
|
|
|
|
|
+
|
|
|
|
|
+ char* cursor = commandLineText + kCommandPrefixLength;
|
|
|
|
|
+ char* commandName = nullptr;
|
|
|
|
|
+ CommandArguments arguments = {};
|
|
|
|
|
+ uint8_t argumentCount = 0;
|
|
|
|
|
+ bool isFirstToken = true;
|
|
|
|
|
+ while (*cursor != '\0') {
|
|
|
|
|
+ while (*cursor == ' ') {
|
|
|
|
|
+ ++cursor;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (*cursor == '\0') {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ char* tokenStart = cursor;
|
|
|
|
|
+ while (*cursor != '\0' && *cursor != ' ') {
|
|
|
|
|
+ ++cursor;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (*cursor == ' ') {
|
|
|
|
|
+ *cursor = '\0';
|
|
|
|
|
+ ++cursor;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isFirstToken) {
|
|
|
|
|
+ commandName = tokenStart;
|
|
|
|
|
+ isFirstToken = false;
|
|
|
|
|
+ } else if (argumentCount < kMaximumArgumentCount) {
|
|
|
|
|
+ arguments.values[argumentCount++] = tokenStart;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (commandName == nullptr) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+#if defined(MECOFFEE_UNO_BLE_TEST)
|
|
|
|
|
+ // Separate opt-in command: preserve the original "cmd reset en" behavior.
|
|
|
|
|
+ if (areTextsEqual(commandName, "bootloader")) {
|
|
|
|
|
+ if (argumentCount == 0) {
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd bootloader uno-test optiboot44 57600 OK\r\n"));
|
|
|
|
|
+ } else if (argumentCount == 1 && areTextsEqual(arguments.values[0], "enter")) {
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd bootloader ready 57600 OK\r\n"));
|
|
|
|
|
+ hardware::enterUnoBleTestBootloader();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd bootloader NOT OK\r\n"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+ for (uint8_t entryIndex = 0; entryIndex < kRegisteredCommandCount; ++entryIndex) {
|
|
|
|
|
+ CommandEntry entryStorage;
|
|
|
|
|
+ const CommandEntry* entry = commandEntryAt(entryIndex, &entryStorage);
|
|
|
|
|
+ if (!isProgramMemoryTextEqual(commandName, entry->name)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("cmd "));
|
|
|
|
|
+ writeRamText(commandName);
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ const bool didSucceed = entry->handler(state, arguments);
|
|
|
|
|
+ if (didSucceed) {
|
|
|
|
|
+ writeSpace();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" NOT "));
|
|
|
|
|
+ }
|
|
|
|
|
+ writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("OK\r\n"));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+void acceptReceivedByte(FirmwareState* state, uint8_t receivedByte) {
|
|
|
|
|
+#if defined(MECOFFEE_RUST_COMMAND_READER)
|
|
|
|
|
+ static_assert(kCommandLineCapacity == 31, "Rust command buffer capacity must match");
|
|
|
|
|
+ // Rust borrows only the 31 payload bytes for this call. Dispatch can mutate
|
|
|
|
|
+ // the buffer and append its NUL only after the Rust borrow has ended.
|
|
|
|
|
+ const uint16_t result = mecoffee_rust_accept_command_byte(
|
|
|
|
|
+ reinterpret_cast<uint8_t*>(state->commandLine), state->commandLineLength, receivedByte);
|
|
|
|
|
+ if ((result & 0x100u) != 0) {
|
|
|
|
|
+ state->commandLineLength = 0;
|
|
|
|
|
+ dispatchCommandLine(state, state->commandLine, static_cast<uint8_t>(result));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state->commandLineLength = static_cast<uint8_t>(result);
|
|
|
|
|
+ }
|
|
|
|
|
+#else
|
|
|
|
|
+ if (receivedByte == 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (receivedByte == '\n') {
|
|
|
|
|
+ const uint8_t completedLength = state->commandLineLength;
|
|
|
|
|
+ state->commandLineLength = 0;
|
|
|
|
|
+ dispatchCommandLine(state, state->commandLine, completedLength);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (state->commandLineLength < kCommandLineCapacity) {
|
|
|
|
|
+ state->commandLine[state->commandLineLength++] = static_cast<char>(receivedByte);
|
|
|
|
|
+ }
|
|
|
|
|
+#endif
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace meCoffee
|