updates
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// A fixed-size, little-endian UART frame shared by the camera and both eyes.
|
||||
namespace gaze {
|
||||
constexpr uint8_t kMagic0 = 0xA5;
|
||||
constexpr uint8_t kMagic1 = 0x5A;
|
||||
constexpr uint8_t kVersion = 1;
|
||||
constexpr uint8_t kValid = 0x01;
|
||||
constexpr size_t kFrameSize = 12;
|
||||
constexpr uint32_t kTimeoutMs = 750;
|
||||
|
||||
struct Frame {
|
||||
uint16_t sequence;
|
||||
uint16_t x; // 0..1000, left to right in the camera image
|
||||
uint16_t y; // 0..1000, top to bottom in the camera image
|
||||
bool valid;
|
||||
};
|
||||
|
||||
inline uint16_t crc16(const uint8_t *bytes, size_t length) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc & 0x8000) ? static_cast<uint16_t>((crc << 1) ^ 0x1021)
|
||||
: static_cast<uint16_t>(crc << 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
inline void encode(const Frame &frame, uint8_t out[kFrameSize]) {
|
||||
out[0] = kMagic0;
|
||||
out[1] = kMagic1;
|
||||
out[2] = kVersion;
|
||||
out[3] = frame.valid ? kValid : 0;
|
||||
out[4] = static_cast<uint8_t>(frame.sequence);
|
||||
out[5] = static_cast<uint8_t>(frame.sequence >> 8);
|
||||
out[6] = static_cast<uint8_t>(frame.x);
|
||||
out[7] = static_cast<uint8_t>(frame.x >> 8);
|
||||
out[8] = static_cast<uint8_t>(frame.y);
|
||||
out[9] = static_cast<uint8_t>(frame.y >> 8);
|
||||
const uint16_t crc = crc16(out, kFrameSize - 2);
|
||||
out[10] = static_cast<uint8_t>(crc);
|
||||
out[11] = static_cast<uint8_t>(crc >> 8);
|
||||
}
|
||||
|
||||
inline bool decode(const uint8_t bytes[kFrameSize], Frame *out) {
|
||||
if (bytes[0] != kMagic0 || bytes[1] != kMagic1 ||
|
||||
bytes[2] != kVersion || (bytes[3] & ~kValid) != 0) return false;
|
||||
const uint16_t actual_crc = static_cast<uint16_t>(bytes[10]) |
|
||||
(static_cast<uint16_t>(bytes[11]) << 8);
|
||||
if (crc16(bytes, kFrameSize - 2) != actual_crc) return false;
|
||||
const uint16_t x = static_cast<uint16_t>(bytes[6]) |
|
||||
(static_cast<uint16_t>(bytes[7]) << 8);
|
||||
const uint16_t y = static_cast<uint16_t>(bytes[8]) |
|
||||
(static_cast<uint16_t>(bytes[9]) << 8);
|
||||
if (x > 1000 || y > 1000) return false;
|
||||
out->sequence = static_cast<uint16_t>(bytes[4]) |
|
||||
(static_cast<uint16_t>(bytes[5]) << 8);
|
||||
out->x = x;
|
||||
out->y = y;
|
||||
out->valid = (bytes[3] & kValid) != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
class Receiver {
|
||||
public:
|
||||
// Returns true only for a complete, valid, fresh frame.
|
||||
bool feed(uint8_t byte, uint32_t now_ms, Frame *out) {
|
||||
if (length_ == 0 && byte != kMagic0) return false;
|
||||
if (length_ == 1 && byte != kMagic1) {
|
||||
length_ = byte == kMagic0 ? 1 : 0;
|
||||
return false;
|
||||
}
|
||||
bytes_[length_++] = byte;
|
||||
if (length_ < kFrameSize) return false;
|
||||
|
||||
Frame decoded = {};
|
||||
const bool well_formed = decode(bytes_, &decoded);
|
||||
if (well_formed) {
|
||||
length_ = 0;
|
||||
} else {
|
||||
++rejected_format_;
|
||||
// Keep an embedded preamble, or a trailing first magic byte.
|
||||
size_t start = 1;
|
||||
while (start < kFrameSize &&
|
||||
(bytes_[start] != kMagic0 ||
|
||||
(start + 1 < kFrameSize && bytes_[start + 1] != kMagic1))) {
|
||||
++start;
|
||||
}
|
||||
length_ = kFrameSize - start;
|
||||
for (size_t i = 0; i < length_; ++i) bytes_[i] = bytes_[start + i];
|
||||
}
|
||||
if (!well_formed) return false;
|
||||
|
||||
if (has_sequence_ && static_cast<uint32_t>(now_ms - last_ms_) <= kTimeoutMs) {
|
||||
const uint16_t advance = static_cast<uint16_t>(decoded.sequence - last_sequence_);
|
||||
if (advance == 0 || advance >= 0x8000) {
|
||||
++rejected_sequence_;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
has_sequence_ = true;
|
||||
last_sequence_ = decoded.sequence;
|
||||
last_ms_ = now_ms;
|
||||
*out = decoded;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool recent(uint32_t now_ms) const {
|
||||
return has_sequence_ && static_cast<uint32_t>(now_ms - last_ms_) <= kTimeoutMs;
|
||||
}
|
||||
|
||||
uint32_t rejected_format() const { return rejected_format_; }
|
||||
uint32_t rejected_sequence() const { return rejected_sequence_; }
|
||||
|
||||
private:
|
||||
uint8_t bytes_[kFrameSize] = {};
|
||||
size_t length_ = 0;
|
||||
bool has_sequence_ = false;
|
||||
uint16_t last_sequence_ = 0;
|
||||
uint32_t last_ms_ = 0;
|
||||
uint32_t rejected_format_ = 0;
|
||||
uint32_t rejected_sequence_ = 0;
|
||||
};
|
||||
} // namespace gaze
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include "mbedtls/md.h"
|
||||
|
||||
namespace freddy_ota {
|
||||
|
||||
inline bool is_hex(const char *value, size_t length) {
|
||||
if (!value || strlen(value) != length) return false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const char c = value[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void to_hex(const uint8_t *bytes, size_t size, char *out) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
out[2 * i] = digits[bytes[i] >> 4];
|
||||
out[2 * i + 1] = digits[bytes[i] & 15];
|
||||
}
|
||||
out[2 * size] = 0;
|
||||
}
|
||||
|
||||
inline bool constant_time_equal(const char *a, const char *b, size_t size) {
|
||||
unsigned difference = 0;
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
difference |= static_cast<unsigned>(a[i] ^ b[i]);
|
||||
return difference == 0;
|
||||
}
|
||||
|
||||
// Authentication binds one upload to its device, one-time nonce, image digest,
|
||||
// and exact binary length. The key is never sent over the network.
|
||||
inline bool verify(const char *key, const char *device, const char *nonce,
|
||||
const char *sha256, size_t length, const char *signature) {
|
||||
if (!key || strlen(key) < 32 || !is_hex(nonce, 32) ||
|
||||
!is_hex(sha256, 64) || !is_hex(signature, 64)) return false;
|
||||
char message[160];
|
||||
const int n = snprintf(message, sizeof(message), "%s\n%s\n%s\n%zu",
|
||||
device, nonce, sha256, length);
|
||||
if (n < 0 || static_cast<size_t>(n) >= sizeof(message)) return false;
|
||||
const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
if (!md) return false;
|
||||
uint8_t digest[32];
|
||||
if (mbedtls_md_hmac(md, reinterpret_cast<const uint8_t *>(key), strlen(key),
|
||||
reinterpret_cast<const uint8_t *>(message), n,
|
||||
digest) != 0) return false;
|
||||
char expected[65];
|
||||
to_hex(digest, sizeof(digest), expected);
|
||||
return constant_time_equal(expected, signature, 64);
|
||||
}
|
||||
|
||||
} // namespace freddy_ota
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// Generated locally by firmware/tools/configure_ota.py; never commit credentials.
|
||||
#if __has_include("../private/OtaSecrets.h")
|
||||
#include "../private/OtaSecrets.h"
|
||||
#else
|
||||
#define FREDDY_WIFI_SSID ""
|
||||
#define FREDDY_WIFI_PASSWORD ""
|
||||
#define FREDDY_OTA_KEY ""
|
||||
#endif
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// One camera-side decision is broadcast to both eyes so brief model misses do
|
||||
// not make their appearance and gaze repeatedly switch between TRACK and IDLE.
|
||||
namespace tracking {
|
||||
constexpr unsigned kConfirmFrames = 3;
|
||||
constexpr uint32_t kTargetHoldMs = 1000;
|
||||
|
||||
struct Decision {
|
||||
int x;
|
||||
int y;
|
||||
bool valid;
|
||||
bool held;
|
||||
};
|
||||
|
||||
class Gate {
|
||||
public:
|
||||
Decision update(bool detected, int x, int y, uint32_t now_ms) {
|
||||
if (detected) {
|
||||
if (candidate_count_ && abs(x - candidate_x_) <= 80 &&
|
||||
abs(y - candidate_y_) <= 60) {
|
||||
if (candidate_count_ < kConfirmFrames) ++candidate_count_;
|
||||
} else {
|
||||
candidate_count_ = 1;
|
||||
}
|
||||
candidate_x_ = x;
|
||||
candidate_y_ = y;
|
||||
if (candidate_count_ >= kConfirmFrames) {
|
||||
locked_ = true;
|
||||
last_confirmed_ms_ = now_ms;
|
||||
locked_x_ = x;
|
||||
locked_y_ = y;
|
||||
return {x, y, true, false};
|
||||
}
|
||||
}
|
||||
|
||||
if (locked_ && static_cast<uint32_t>(now_ms - last_confirmed_ms_) <=
|
||||
kTargetHoldMs) {
|
||||
return {locked_x_, locked_y_, true, true};
|
||||
}
|
||||
|
||||
locked_ = false;
|
||||
if (!detected) {
|
||||
candidate_count_ = 0;
|
||||
candidate_x_ = -1;
|
||||
candidate_y_ = -1;
|
||||
return {500, 500, false, false};
|
||||
}
|
||||
return {x, y, false, false};
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned candidate_count_ = 0;
|
||||
int candidate_x_ = -1;
|
||||
int candidate_y_ = -1;
|
||||
bool locked_ = false;
|
||||
int locked_x_ = 500;
|
||||
int locked_y_ = 500;
|
||||
uint32_t last_confirmed_ms_ = 0;
|
||||
};
|
||||
} // namespace tracking
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "GazeProtocol.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace {
|
||||
bool feed_frame(gaze::Receiver *receiver, const uint8_t *bytes,
|
||||
uint32_t now_ms, gaze::Frame *out) {
|
||||
bool accepted = false;
|
||||
for (size_t i = 0; i < gaze::kFrameSize; ++i) {
|
||||
accepted |= receiver->feed(bytes[i], now_ms, out);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
gaze::Frame sent = {65535, 0, 1000, true};
|
||||
uint8_t bytes[gaze::kFrameSize];
|
||||
gaze::encode(sent, bytes);
|
||||
gaze::Frame received = {};
|
||||
assert(gaze::decode(bytes, &received));
|
||||
assert(received.sequence == sent.sequence && received.x == 0 &&
|
||||
received.y == 1000 && received.valid);
|
||||
|
||||
gaze::Receiver receiver;
|
||||
assert(feed_frame(&receiver, bytes, 100, &received));
|
||||
assert(receiver.recent(850));
|
||||
assert(!receiver.recent(851));
|
||||
assert(!feed_frame(&receiver, bytes, 200, &received)); // duplicate
|
||||
assert(receiver.rejected_sequence() == 1);
|
||||
|
||||
sent.sequence = 0; // wrap-around
|
||||
sent.x = 500;
|
||||
gaze::encode(sent, bytes);
|
||||
assert(feed_frame(&receiver, bytes, 200, &received));
|
||||
assert(received.sequence == 0 && received.x == 500);
|
||||
|
||||
sent.sequence = 65535; // out of order
|
||||
gaze::encode(sent, bytes);
|
||||
assert(!feed_frame(&receiver, bytes, 300, &received));
|
||||
assert(receiver.rejected_sequence() == 2);
|
||||
assert(feed_frame(&receiver, bytes, 1000, &received)); // reboot after timeout
|
||||
|
||||
bytes[2] = 2; // unsupported version
|
||||
assert(!gaze::decode(bytes, &received));
|
||||
gaze::encode(sent, bytes);
|
||||
bytes[6] ^= 1; // bad checksum
|
||||
assert(!feed_frame(&receiver, bytes, 1100, &received));
|
||||
assert(receiver.rejected_format() == 1);
|
||||
|
||||
sent.sequence = 1;
|
||||
sent.valid = false;
|
||||
gaze::encode(sent, bytes);
|
||||
receiver.feed(0xA5, 1100, &received); // truncated preamble
|
||||
receiver.feed(0x00, 1100, &received);
|
||||
assert(feed_frame(&receiver, bytes, 1100, &received));
|
||||
assert(!received.valid);
|
||||
|
||||
sent.x = 1001;
|
||||
gaze::encode(sent, bytes);
|
||||
assert(!gaze::decode(bytes, &received));
|
||||
puts("gaze protocol: round trip, CRC, version, range, resync, sequence, timeout OK");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "TrackingGate.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
tracking::Gate gate;
|
||||
assert(!gate.update(false, 0, 0, 0).valid);
|
||||
assert(!gate.update(true, 160, 100, 250).valid);
|
||||
assert(!gate.update(true, 165, 105, 500).valid);
|
||||
const auto first_lock = gate.update(true, 170, 110, 750);
|
||||
assert(first_lock.valid && !first_lock.held && first_lock.x == 170);
|
||||
|
||||
// Two missed model results keep both eyes tracking the last known position.
|
||||
const auto missed = gate.update(false, 0, 0, 1000);
|
||||
assert(missed.valid && missed.held && missed.x == 170 && missed.y == 110);
|
||||
assert(gate.update(false, 0, 0, 1250).valid);
|
||||
const auto reacquired = gate.update(true, 173, 108, 1500);
|
||||
assert(reacquired.valid && !reacquired.held && reacquired.x == 173);
|
||||
|
||||
// A new distant candidate cannot yank the gaze until confirmed.
|
||||
const auto other = gate.update(true, 290, 200, 1750);
|
||||
assert(other.valid && other.held && other.x == 173);
|
||||
assert(gate.update(true, 292, 201, 2000).held);
|
||||
const auto switched = gate.update(true, 294, 202, 2250);
|
||||
assert(switched.valid && !switched.held && switched.x == 294);
|
||||
|
||||
// Once the person leaves, the brief hold expires and blue/IDLE returns.
|
||||
assert(gate.update(false, 0, 0, 3250).valid);
|
||||
assert(!gate.update(false, 0, 0, 3251).valid);
|
||||
assert(!gate.update(true, 160, 100, 3500).valid);
|
||||
assert(!gate.update(true, 162, 102, 3750).valid);
|
||||
assert(gate.update(true, 164, 104, 4000).valid);
|
||||
puts("tracking gate: confirmation, brief misses, candidate switch, expiry OK");
|
||||
}
|
||||
Reference in New Issue
Block a user