Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions include/packet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once

#include <cstddef>
#include <cstdint>

#include "battery.h"

// Pure encode/decode for Snips' own application-level payload — what goes
// inside an XBee Transmit Request (0x10) / Receive Packet (0x90), defined
// fresh since Amidala has no existing schema for this controller type
// (see thePunderWoman/Amidala#204). Knows nothing about XBee framing
// itself — see xbee_frame.h for that — or about what any of these values
// mean to the rest of the firmware or to Amidala.
//
// Field widths and the wire format (fixed-width, big-endian for
// multi-byte fields) are this rewrite's own choice, not dictated by any
// existing spec.

// Controller -> Amidala, sent periodically. Raw per-button state (no
// gesture classification — see the rewrite plan's Context section) plus
// calibrated analog and battery/charge status.
struct UplinkPacket {
uint16_t buttonMask = 0; // bit i = Buttons::Index i is pressed
uint8_t triggerPercent = 0; // 0-100
int8_t stickXPercent = 0; // -100..100
int8_t stickYPercent = 0; // -100..100
uint8_t batteryPercent = 0; // 0-100
ChargeState chargeState = ChargeState::kDone;
};

// Amidala -> controller. Handedness is sent once at connect and is static
// for the session; the Left/Right slot label+value are generic — Amidala
// assigns their meaning (volume, throttle, or anything else) and echoes
// back whatever the current label/value should read.
struct DownlinkPacket {
enum class Handedness : uint8_t { kUnknown = 0, kLeft = 1, kRight = 2 };

static constexpr size_t kFieldLength = 8;

Handedness handedness = Handedness::kUnknown;
char leftLabel[kFieldLength + 1] = {};
char leftValue[kFieldLength + 1] = {};
char rightLabel[kFieldLength + 1] = {};
char rightValue[kFieldLength + 1] = {};
};

namespace Packet {

constexpr size_t kUplinkEncodedSize = 7;
constexpr size_t kDownlinkEncodedSize =
1 + 4 * DownlinkPacket::kFieldLength; // 33

// Returns the number of bytes written (always kUplinkEncodedSize), or 0
// if outCapacity is too small.
size_t encodeUplink(const UplinkPacket &packet, uint8_t *outBuf,
size_t outCapacity);

// Returns false if length is too short to hold a full uplink packet.
bool decodeUplink(const uint8_t *buf, size_t length, UplinkPacket *out);

// Returns the number of bytes written (always kDownlinkEncodedSize), or 0
// if outCapacity is too small.
size_t encodeDownlink(const DownlinkPacket &packet, uint8_t *outBuf,
size_t outCapacity);

// Returns false if length is too short to hold a full downlink packet.
bool decodeDownlink(const uint8_t *buf, size_t length, DownlinkPacket *out);

} // namespace Packet
12 changes: 12 additions & 0 deletions include/xbee_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ class XbeeControl : public XbeeTransport {
// query failure, leaving outHex untouched.
bool querySerialLow(char *outHex, size_t outHexCapacity);

// Passthroughs to the owned XbeeSpi — see xbee_spi.h. XbeeControl is
// kept as the single facade over the one physical XBee connection
// rather than SnipsController.ino owning a second XbeeSpi instance.
void sendPacket(const uint8_t *payload, uint16_t payloadLength) {
spi_.sendPacket(payload, payloadLength);
}
bool pollForPacket(uint8_t *outPayload, uint16_t outPayloadCapacity,
uint16_t *outPayloadLength) {
return spi_.pollForPacket(outPayload, outPayloadCapacity,
outPayloadLength);
}

private:
XbeeSpi spi_;
};
33 changes: 33 additions & 0 deletions include/xbee_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,37 @@ struct AtCommandResponse {
bool parseAtCommandResponse(const uint8_t *frameData, uint16_t length,
AtCommandResponse *out);

constexpr uint8_t kFrameTypeTransmitRequest = 0x10;
constexpr uint8_t kFrameTypeReceivePacket = 0x90;

// The ZigBee coordinator's network address is always 0x0000; per Digi's
// convention, a 64-bit destination of all-zero paired with this means
// "route by 16-bit address, 64-bit unknown" — i.e. exactly "send to the
// coordinator" without needing to know its actual 64-bit address. Not yet
// validated against real hardware — flagged for this PR's bring-up.
constexpr uint64_t kCoordinatorAddress64 = 0;
constexpr uint16_t kCoordinatorAddress16 = 0x0000;

// Builds a Transmit Request frame's data (type 0x10) into `outFrameData`,
// addressed to dest64/dest16 (default: the coordinator, see above).
// Returns the number of bytes written, or 0 if outCapacity is too small.
uint16_t buildTransmitRequestFrame(uint8_t *outFrameData,
uint16_t outCapacity, uint8_t frameId,
const uint8_t *payload,
uint16_t payloadLength,
uint64_t dest64 = kCoordinatorAddress64,
uint16_t dest16 = kCoordinatorAddress16);

struct ReceivePacket {
uint64_t sourceAddress64;
const uint8_t *payload; // points into the buffer passed to parse()
uint16_t payloadLength;
};

// Parses a Receive Packet frame's data (type 0x90). `frameData` must stay
// valid as long as `out->payload` is used. Returns false if `frameData`
// isn't a recognized/well-formed Receive Packet.
bool parseReceivePacket(const uint8_t *frameData, uint16_t length,
ReceivePacket *out);

} // namespace XbeeFrame
14 changes: 14 additions & 0 deletions include/xbee_spi.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@ class XbeeSpi {
uint8_t valueLength, uint8_t *outValue,
uint8_t outValueCapacity, uint8_t *outValueLength,
unsigned long timeoutMs = 200);

// Sends our own application payload to the coordinator as a Transmit
// Request (0x10) — non-blocking, fire-and-forget (no response is
// expected or waited for; XBee's own ACK/retry handles reliability at
// the radio level).
void sendPacket(const uint8_t *payload, uint16_t payloadLength);

// Non-blocking: drains any queued frames, returning the payload of the
// first Receive Packet (0x90) found (frames of any other type are
// silently discarded — this firmware only expects AT Command Responses,
// handled synchronously by sendAtCommand(), or Receive Packets here).
// Returns false if nothing was available this call.
bool pollForPacket(uint8_t *outPayload, uint16_t outPayloadCapacity,
uint16_t *outPayloadLength);
};
96 changes: 73 additions & 23 deletions src/SnipsController.ino
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "droid_persistence.h"
#include "menu.h"
#include "oled.h"
#include "packet.h"
#include "pin_assignment.h"
#include "power_latch.h"
#include "rgb_led.h"
Expand All @@ -33,6 +34,8 @@ char deviceSerialLowBuf[9] = {}; // must outlive setup() — see its use below
bool lastReportedPressed[Buttons::kCount] = {};
unsigned long lastTelemetryLogMs = 0;
constexpr unsigned long kTelemetryLogIntervalMs = 1000;
unsigned long lastUplinkSendMs = 0;
constexpr unsigned long kUplinkSendIntervalMs = 50;
MenuScreen previousMenuScreen = MenuScreen::kInactive;
MainMenuItem previousMainMenuItem = MainMenuItem::kSwitchDroid;

Expand Down Expand Up @@ -94,8 +97,7 @@ void setup() {
// All buttons wire to GND with the internal pull-up enabled, so LOW =
// pressed. No classification happens here — Amidala owns single/double/
// long-press and alt semantics centrally; this firmware only reports
// debounced raw press/release (packet protocol lands in a later PR, so
// for now state changes are just logged for bring-up).
// debounced raw press/release, via the uplink packet below.
for (size_t i = 0; i < Buttons::kCount; ++i) {
pinMode(Buttons::kPins[i], INPUT_PULLUP);
}
Expand Down Expand Up @@ -164,10 +166,10 @@ void loop() {

if (powerOffDetector.update(powerButtonHeld, now)) {
// The real graceful-shutdown sequence (notify Amidala, OLED message,
// then drive the latch pin low) lands in a later PR once the packet
// protocol and display exist. For now, just prove the hold is detected.
// then drive the latch pin low) lands in PR 10. For now, just prove
// the hold is detected.
Serial.println(
"Power button held 3s - shutdown sequence would run here (PR 9).");
"Power button held 3s - shutdown sequence would run here (PR 10).");
}

// Read every tick (not just on the telemetry throttle below) — the menu
Expand Down Expand Up @@ -262,27 +264,75 @@ void loop() {
}
}

// Packet protocol lands in PR 8 — for now, just log periodically (not
// every tick) so bring-up can confirm these readings look right.
const int rawVsys = analogRead(PinAssignment::kVsysSense);
const bool stat1High = digitalRead(PinAssignment::kChargeStat1) == HIGH;
const bool stat2High = digitalRead(PinAssignment::kChargeStat2) == HIGH;

const int triggerPercent =
AnalogCalibration::calibrateTrigger(rawTrigger, calibrationData);
const int stickXPercent = AnalogCalibration::calibrateStickAxis(
rawStickX, calibrationData.stickXMin, calibrationData.stickXCenter,
calibrationData.stickXMax);
const int stickYPercent = AnalogCalibration::calibrateStickAxis(
rawStickY, calibrationData.stickYMin, calibrationData.stickYCenter,
calibrationData.stickYMax);
const int batteryPercent = batteryMonitor.percentFor(rawVsys);
const ChargeState chargeState =
batteryMonitor.chargeStateFor(stat1High, stat2High);

// Uplink: sent periodically over the radio, faster than the
// human-readable Serial telemetry below — this is what Amidala
// actually sees.
if (now - lastUplinkSendMs >= kUplinkSendIntervalMs) {
lastUplinkSendMs = now;

UplinkPacket uplink;
for (size_t i = 0; i < Buttons::kCount; ++i) {
if (buttonPanel.isPressed(i)) {
uplink.buttonMask |= static_cast<uint16_t>(1u << i);
}
}
uplink.triggerPercent = static_cast<uint8_t>(triggerPercent);
uplink.stickXPercent = static_cast<int8_t>(stickXPercent);
uplink.stickYPercent = static_cast<int8_t>(stickYPercent);
uplink.batteryPercent = static_cast<uint8_t>(batteryPercent);
uplink.chargeState = chargeState;

uint8_t uplinkBuf[Packet::kUplinkEncodedSize];
const size_t uplinkLength =
Packet::encodeUplink(uplink, uplinkBuf, sizeof(uplinkBuf));
if (uplinkLength > 0) {
xbeeControl.sendPacket(uplinkBuf, static_cast<uint16_t>(uplinkLength));
}
}

// Downlink: non-blocking poll every tick. Nothing consumes handedness
// or the Left/Right label+value yet — that's the complications system,
// PR 10 — so for now this just proves the round trip works.
uint8_t downlinkBuf[Packet::kDownlinkEncodedSize];
uint16_t downlinkLength = 0;
if (xbeeControl.pollForPacket(downlinkBuf, sizeof(downlinkBuf),
&downlinkLength)) {
DownlinkPacket downlink;
if (Packet::decodeDownlink(downlinkBuf, downlinkLength, &downlink)) {
Serial.print("Downlink: hand=");
Serial.print(static_cast<int>(downlink.handedness));
Serial.print(" L=");
Serial.print(downlink.leftLabel);
Serial.print(":");
Serial.print(downlink.leftValue);
Serial.print(" R=");
Serial.print(downlink.rightLabel);
Serial.print(":");
Serial.println(downlink.rightValue);
}
}

// Human-readable Serial telemetry — not what Amidala sees, just a
// slower-cadence bring-up check that the values above look right.
if (now - lastTelemetryLogMs >= kTelemetryLogIntervalMs) {
lastTelemetryLogMs = now;

const int rawVsys = analogRead(PinAssignment::kVsysSense);
const bool stat1High = digitalRead(PinAssignment::kChargeStat1) == HIGH;
const bool stat2High = digitalRead(PinAssignment::kChargeStat2) == HIGH;

const int triggerPercent =
AnalogCalibration::calibrateTrigger(rawTrigger, calibrationData);
const int stickXPercent = AnalogCalibration::calibrateStickAxis(
rawStickX, calibrationData.stickXMin, calibrationData.stickXCenter,
calibrationData.stickXMax);
const int stickYPercent = AnalogCalibration::calibrateStickAxis(
rawStickY, calibrationData.stickYMin, calibrationData.stickYCenter,
calibrationData.stickYMax);
const int batteryPercent = batteryMonitor.percentFor(rawVsys);
const ChargeState chargeState =
batteryMonitor.chargeStateFor(stat1High, stat2High);

Serial.print("Battery ");
Serial.print(batteryPercent);
Serial.print("% (");
Expand Down
84 changes: 84 additions & 0 deletions src/packet.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include "packet.h"

namespace {

// Copies a null-terminated string into a fixed-width field, truncating if
// too long and zero-padding if shorter (so the decode side's strings come
// back clean, terminated at the first zero either way).
void writeField(const char *text, uint8_t *outField, size_t fieldLength) {
size_t i = 0;
for (; i < fieldLength && text[i] != '\0'; i++) {
outField[i] = static_cast<uint8_t>(text[i]);
}
for (; i < fieldLength; i++) {
outField[i] = 0;
}
}

// Reads a fixed-width field back into a null-terminated buffer (which
// must be at least fieldLength + 1 bytes).
void readField(const uint8_t *field, size_t fieldLength, char *outText) {
for (size_t i = 0; i < fieldLength; i++) {
outText[i] = static_cast<char>(field[i]);
}
outText[fieldLength] = '\0';
}

} // namespace

size_t Packet::encodeUplink(const UplinkPacket &packet, uint8_t *outBuf,
size_t outCapacity) {
if (outCapacity < kUplinkEncodedSize) {
return 0;
}
outBuf[0] = static_cast<uint8_t>(packet.buttonMask >> 8);
outBuf[1] = static_cast<uint8_t>(packet.buttonMask & 0xFF);
outBuf[2] = packet.triggerPercent;
outBuf[3] = static_cast<uint8_t>(packet.stickXPercent);
outBuf[4] = static_cast<uint8_t>(packet.stickYPercent);
outBuf[5] = packet.batteryPercent;
outBuf[6] = static_cast<uint8_t>(packet.chargeState);
return kUplinkEncodedSize;
}

bool Packet::decodeUplink(const uint8_t *buf, size_t length,
UplinkPacket *out) {
if (length < kUplinkEncodedSize) {
return false;
}
out->buttonMask = (static_cast<uint16_t>(buf[0]) << 8) | buf[1];
out->triggerPercent = buf[2];
out->stickXPercent = static_cast<int8_t>(buf[3]);
out->stickYPercent = static_cast<int8_t>(buf[4]);
out->batteryPercent = buf[5];
out->chargeState = static_cast<ChargeState>(buf[6]);
return true;
}

size_t Packet::encodeDownlink(const DownlinkPacket &packet, uint8_t *outBuf,
size_t outCapacity) {
if (outCapacity < kDownlinkEncodedSize) {
return 0;
}
constexpr size_t kF = DownlinkPacket::kFieldLength;
outBuf[0] = static_cast<uint8_t>(packet.handedness);
writeField(packet.leftLabel, outBuf + 1, kF);
writeField(packet.leftValue, outBuf + 1 + kF, kF);
writeField(packet.rightLabel, outBuf + 1 + 2 * kF, kF);
writeField(packet.rightValue, outBuf + 1 + 3 * kF, kF);
return kDownlinkEncodedSize;
}

bool Packet::decodeDownlink(const uint8_t *buf, size_t length,
DownlinkPacket *out) {
if (length < kDownlinkEncodedSize) {
return false;
}
constexpr size_t kF = DownlinkPacket::kFieldLength;
out->handedness = static_cast<DownlinkPacket::Handedness>(buf[0]);
readField(buf + 1, kF, out->leftLabel);
readField(buf + 1 + kF, kF, out->leftValue);
readField(buf + 1 + 2 * kF, kF, out->rightLabel);
readField(buf + 1 + 3 * kF, kF, out->rightValue);
return true;
}
Loading
Loading