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
5 changes: 5 additions & 0 deletions include/buttons.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ constexpr int kPins[kCount] = {
PinAssignment::kTriggerUp, PinAssignment::kTriggerDown,
};

// Human-readable name for a button index, e.g. for the on-device Button
// Test screen (see menu.h) and Serial press/release logging. Out-of-range
// indices return "Unknown" rather than being undefined.
const char *name(size_t index);

} // namespace Buttons

// Debounces a single button's raw (noisy) pin reading over time. A state
Expand Down
12 changes: 12 additions & 0 deletions include/firmware_version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

// Injected by scripts/firmware_version.py as a `git describe` string for
// the esp32s3 build; the native test build never defines it, so it falls
// back to a fixed placeholder there.
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "dev"
#endif

namespace Firmware {
constexpr const char *kVersion = FIRMWARE_VERSION;
} // namespace Firmware
17 changes: 17 additions & 0 deletions include/menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ enum class MenuScreen {
kDisplayConfig,
kPowerConfig,
kDeviceInfo,
kButtonTest,
kFactoryResetConfirm,
};

Expand All @@ -38,6 +39,7 @@ enum class MainMenuItem {
kDisplayConfig,
kPowerConfig,
kDeviceInfo,
kButtonTest,
kFactoryReset,
kCount,
};
Expand Down Expand Up @@ -101,6 +103,19 @@ class MenuController {
int *outMaxY);
bool consumeFactoryResetConfirmed();

// Button Test screen (bring-up/support tool — see PCB/README.md's
// Bringup Sequence, step 4): records the most recent raw button press
// for display, by index into Buttons::Index. While this screen is
// active, SnipsController.ino routes every button's press edge here
// instead of through the usual nav calls (onUp()/onDown()/onEnter()),
// including the ones normally "stolen" for nav everywhere else —
// except Bumper, which still exits the screen via onBack() same as any
// other screen; that exit is itself the test for Bumper. No-op unless
// this screen is active. -1 means "nothing pressed yet since the
// screen was opened".
void onButtonTestPress(size_t buttonIndex);
int lastTestedButtonIndex() const { return lastTestedButtonIndex_; }

// Droid management — the store is owned here so rendering/navigation
// can see it directly; SnipsController.ino restores it from
// DroidPersistence once at boot and re-persists it whenever
Expand Down Expand Up @@ -186,6 +201,8 @@ class MenuController {

bool factoryResetConfirmed_ = false;

int lastTestedButtonIndex_ = -1;

DroidStore droidStore_;
int droidListIndex_ = 0;
bool droidStoreChanged_ = false;
Expand Down
6 changes: 6 additions & 0 deletions include/oled.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class OledDisplay {

void render(const ScreenBuffer &content);

// Draws a 1-bit bitmap (row-major, MSB-first-per-byte, e.g.
// snips_logo_bitmap.h) centered on the display and pushes it immediately.
// Used for the boot splash; not part of the ScreenBuffer content model
// since nothing else on the device draws bitmaps.
void renderBitmapCentered(const uint8_t *bitmap, int width, int height);

// Low-power mode support (see power_management.h). Dimming/undimming
// and powering the panel back on take effect immediately; they don't
// need a render() call to show existing content again since the
Expand Down
54 changes: 54 additions & 0 deletions include/snips_logo_bitmap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#include <cstdint>

// 1-bit Snips logo bitmap for the boot splash, generated from
// graphics/snips-logo.png (itself a 1:1 raster of graphics/snips-logo.svg)
// by thresholding its alpha channel at 48x64 — the largest size that fits
// the 128x64 OLED (Oled::kWidth/kHeight) while preserving the source
// canvas's aspect ratio. Regenerate by re-running that conversion if the
// source art changes; this file has no logic of its own, just packed
// pixel data in the row-major, MSB-first-per-byte format
// Adafruit_GFX::drawBitmap() expects.
namespace SnipsLogo {

constexpr int kWidth = 48;
constexpr int kHeight = 64;
constexpr int kByteWidth = (kWidth + 7) / 8;

constexpr uint8_t kBitmap[kByteWidth * kHeight] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x40, 0x08, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x08, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x10, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x30, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x20, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x00,
0x00, 0x00, 0x00, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x80, 0x00,
0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00,
0x00, 0x00, 0x01, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x01, 0xce, 0x00, 0x00,
0x00, 0x00, 0x01, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x03, 0x5c, 0x00, 0x00,
0x00, 0x00, 0x02, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x05, 0xb8, 0x00, 0x00,
0x00, 0x00, 0x0b, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x12, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x24, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x68, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x28, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x34, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x1e, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfc, 0x00, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x00,
0x00, 0x00, 0x39, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x31, 0xc0, 0x00, 0x00,
0x00, 0x00, 0x71, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc0, 0x00, 0x00,
0x00, 0x00, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x80, 0xc0, 0x00, 0x00,
0x00, 0x1f, 0x00, 0xc0, 0x00, 0x00, 0x01, 0xff, 0x00, 0xc0, 0x00, 0x00,
0x07, 0xff, 0x00, 0xc0, 0x00, 0x00, 0x07, 0xff, 0x01, 0xe0, 0x00, 0x00,
0x07, 0xff, 0x01, 0x30, 0x00, 0x00, 0x07, 0xff, 0x02, 0x10, 0x00, 0x00,
0x07, 0xff, 0x86, 0x08, 0x00, 0x00, 0x07, 0xff, 0x84, 0x04, 0x00, 0x00,
0x03, 0xff, 0x88, 0x02, 0x00, 0x00, 0x03, 0xff, 0x90, 0x01, 0x00, 0x00,
0x03, 0xff, 0x30, 0x01, 0x00, 0x00, 0x03, 0xf0, 0x10, 0x02, 0x00, 0x00,
0x03, 0x00, 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00,
0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10, 0x00, 0x00,
0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

} // namespace SnipsLogo
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ lib_deps =
adafruit/Adafruit SSD1306@^2.5.13
adafruit/Adafruit GFX Library@^1.11.11
adafruit/Adafruit NeoPixel@^1.15.4
extra_scripts = pre:scripts/firmware_version.py

; Native (host) environment for unit tests — no hardware required.
; SnipsController.ino pulls in Arduino.h, so it's excluded here; everything
Expand Down
26 changes: 26 additions & 0 deletions scripts/firmware_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import subprocess

Import("env")

# Firmware version shown on the on-device Device Info screen (see
# menu.cpp's kDeviceInfo render case) — the repo has no tag-based release
# process yet, so this is just the current commit, which is still exactly
# what you want on a bring-up device: proof of what's actually flashed.
# Once real version tags exist, `git describe` picks them up automatically
# with no code changes needed here.
def firmware_version():
try:
return (
subprocess.check_output(
["git", "describe", "--tags", "--always", "--dirty"],
cwd=env["PROJECT_DIR"],
stderr=subprocess.DEVNULL,
)
.decode()
.strip()
)
except Exception:
return "unknown"


env.Append(CPPDEFINES=[("FIRMWARE_VERSION", '\\"%s\\"' % firmware_version())])
49 changes: 25 additions & 24 deletions src/SnipsController.ino
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "power_management.h"
#include "rgb_led.h"
#include "screen.h"
#include "snips_logo_bitmap.h"
#include "status_led.h"
#include "xbee_control.h"

Expand Down Expand Up @@ -140,24 +141,6 @@ void performGracefulShutdown() {
}
}

const char *buttonName(size_t index) {
switch (index) {
case Buttons::kMacro1: return "Macro1";
case Buttons::kMacro2: return "Macro2";
case Buttons::kMacro3: return "Macro3";
case Buttons::kMacro4: return "Macro4";
case Buttons::kMacro5: return "Macro5";
case Buttons::kMacro6: return "Macro6";
case Buttons::kBumper: return "Bumper";
case Buttons::kStickClick: return "StickClick";
case Buttons::kLeftUp: return "LeftUp";
case Buttons::kLeftDown: return "LeftDown";
case Buttons::kRightUp: return "RightUp";
case Buttons::kRightDown: return "RightDown";
default: return "Unknown";
}
}

const char *chargeStateName(ChargeState state) {
switch (state) {
case ChargeState::kDone: return "done";
Expand Down Expand Up @@ -308,9 +291,15 @@ void setup() {
Serial.println("XBee SL query failed at boot.");
}

// Brief boot confirmation — loop() takes over with the real complications
// Power-on splash, so it's obvious the device is booting rather than
// just dark/unresponsive, followed by the existing brief boot
// confirmation text — loop() takes over with the real complications
// screen once real sensor data starts flowing.
if (oledDisplay.begin()) {
oledDisplay.renderBitmapCentered(SnipsLogo::kBitmap, SnipsLogo::kWidth,
SnipsLogo::kHeight);
constexpr unsigned long kBootSplashMs = 1200;
delay(kBootSplashMs);
showBootScreen();
} else {
Serial.println("OLED not found at boot.");
Expand Down Expand Up @@ -360,7 +349,7 @@ void loop() {
justPressed[i] = pressed && !lastReportedPressed[i];
if (pressed != lastReportedPressed[i]) {
lastReportedPressed[i] = pressed;
Serial.print(buttonName(i));
Serial.print(Buttons::name(i));
Serial.println(pressed ? " pressed" : " released");
}
}
Expand All @@ -375,10 +364,22 @@ void loop() {
buttonPanel.isPressed(Buttons::kLeftDown),
now);
menuController.tick(rawStickX, rawStickY);
if (justPressed[Buttons::kLeftUp]) menuController.onUp();
if (justPressed[Buttons::kLeftDown]) menuController.onDown();
if (justPressed[Buttons::kStickClick]) {
menuController.onEnter(rawTrigger, rawStickX, rawStickY);
// Button Test (see menu.h's onButtonTestPress()) wants to see every raw
// button press, including the four normally "stolen" for menu nav
// below — so while it's active, route all of them there instead of
// through the usual nav calls. Bumper is the one exception: it still
// exits via onBack() same as every other screen, which doubles as the
// test for Bumper itself.
if (menuController.currentScreen() == MenuScreen::kButtonTest) {
for (size_t i = 0; i < Buttons::kCount; ++i) {
if (justPressed[i]) menuController.onButtonTestPress(i);
}
} else {
if (justPressed[Buttons::kLeftUp]) menuController.onUp();
if (justPressed[Buttons::kLeftDown]) menuController.onDown();
if (justPressed[Buttons::kStickClick]) {
menuController.onEnter(rawTrigger, rawStickX, rawStickY);
}
}
if (justPressed[Buttons::kBumper]) menuController.onBack();

Expand Down
18 changes: 18 additions & 0 deletions src/buttons.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
#include "buttons.h"

const char *Buttons::name(size_t index) {
switch (index) {
case Buttons::kMacro1: return "Macro1";
case Buttons::kMacro2: return "Macro2";
case Buttons::kMacro3: return "Macro3";
case Buttons::kMacro4: return "Macro4";
case Buttons::kMacro5: return "Macro5";
case Buttons::kMacro6: return "Macro6";
case Buttons::kBumper: return "Bumper";
case Buttons::kStickClick: return "StickClick";
case Buttons::kLeftUp: return "LeftUp";
case Buttons::kLeftDown: return "LeftDown";
case Buttons::kRightUp: return "RightUp";
case Buttons::kRightDown: return "RightDown";
default: return "Unknown";
}
}

ButtonDebouncer::ButtonDebouncer(unsigned long debounceMs)
: debounceMs_(debounceMs) {}

Expand Down
42 changes: 41 additions & 1 deletion src/menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include <cstdio>
#include <cstring>

#include "buttons.h"
#include "firmware_version.h"

namespace {
// A 64-bit all-zero PAN ID — Digi's convention for "unconfigured," used
// by Factory Reset to make sure the radio doesn't quietly stay
Expand All @@ -20,6 +23,7 @@ const char *mainMenuItemLabel(MainMenuItem item) {
case MainMenuItem::kDisplayConfig: return "Display Config";
case MainMenuItem::kPowerConfig: return "Power Management";
case MainMenuItem::kDeviceInfo: return "Device Info";
case MainMenuItem::kButtonTest: return "Button Test";
case MainMenuItem::kFactoryReset: return "Factory Reset";
default: return "Unknown";
}
Expand Down Expand Up @@ -169,6 +173,7 @@ void MenuController::onBack() {
case MenuScreen::kDisplayConfig:
case MenuScreen::kPowerConfig:
case MenuScreen::kDeviceInfo:
case MenuScreen::kButtonTest:
case MenuScreen::kFactoryResetConfirm:
screen_ = MenuScreen::kMainMenu;
break;
Expand Down Expand Up @@ -206,6 +211,10 @@ void MenuController::enterMainMenuItem(MainMenuItem item) {
case MainMenuItem::kDeviceInfo:
screen_ = MenuScreen::kDeviceInfo;
break;
case MainMenuItem::kButtonTest:
lastTestedButtonIndex_ = -1;
screen_ = MenuScreen::kButtonTest;
break;
case MainMenuItem::kFactoryReset:
screen_ = MenuScreen::kFactoryResetConfirm;
break;
Expand Down Expand Up @@ -349,6 +358,13 @@ void MenuController::onEnter(int rawTrigger, int rawStickX, int rawStickY) {
screen_ = MenuScreen::kMainMenu;
break;

// Not reached in normal operation — SnipsController.ino routes button
// presses to onButtonTestPress() instead of onEnter() while this
// screen is active (see its declaration in menu.h). Kept as an
// explicit no-op case for switch exhaustiveness.
case MenuScreen::kButtonTest:
break;

case MenuScreen::kFactoryResetConfirm:
factoryResetConfirmed_ = true;
droidStore_ = DroidStore();
Expand All @@ -374,6 +390,12 @@ void MenuController::onEnter(int rawTrigger, int rawStickX, int rawStickY) {
}
}

void MenuController::onButtonTestPress(size_t buttonIndex) {
if (screen_ != MenuScreen::kButtonTest) return;
if (buttonIndex >= Buttons::kCount) return;
lastTestedButtonIndex_ = static_cast<int>(buttonIndex);
}

void MenuController::tick(int rawStickX, int rawStickY) {
if (screen_ == MenuScreen::kCalibrateStick &&
stickFlow_.currentStep() == StickCalibrationFlow::Step::kRolling) {
Expand Down Expand Up @@ -678,10 +700,28 @@ void renderMenuScreen(const MenuController &menu, ScreenBuffer *screen) {
break;
}

case MenuScreen::kDeviceInfo:
case MenuScreen::kDeviceInfo: {
screen->setLine(0, "Device Info");
screen->setLine(1, "XBee SL:");
screen->setLine(2, menu.deviceSerialLow());
char line[ScreenBuffer::kMaxLineLength + 1];
std::snprintf(line, sizeof(line), "FW: %s", Firmware::kVersion);
screen->setLine(3, line);
break;
}

case MenuScreen::kButtonTest:
screen->setLine(0, "Button Test");
if (menu.lastTestedButtonIndex() < 0) {
screen->setLine(1, "Press any button");
} else {
char line[ScreenBuffer::kMaxLineLength + 1];
std::snprintf(
line, sizeof(line), "Pressed: %s",
Buttons::name(static_cast<size_t>(menu.lastTestedButtonIndex())));
screen->setLine(1, line);
}
screen->setLine(3, "Bumper = exit");
break;

case MenuScreen::kFactoryResetConfirm:
Expand Down
9 changes: 9 additions & 0 deletions src/oled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ void OledDisplay::render(const ScreenBuffer &content) {
display_.display();
}

void OledDisplay::renderBitmapCentered(const uint8_t *bitmap, int width,
int height) {
display_.clearDisplay();
const int16_t x = static_cast<int16_t>((Oled::kWidth - width) / 2);
const int16_t y = static_cast<int16_t>((Oled::kHeight - height) / 2);
display_.drawBitmap(x, y, bitmap, width, height, SSD1306_WHITE);
display_.display();
}

void OledDisplay::setDimmed(bool dimmed) { display_.dim(dimmed); }

void OledDisplay::setPowerOn(bool on) {
Expand Down
Loading
Loading