// --------------------------------------------------------------------------- // Fencing scoring box - portable decision logic // // Deliberately free of Arduino dependencies so the state machine can be // compiled and unit tested on a PC. The sketch supplies timestamps and // classified contacts; everything here is pure logic. // // Timing (FIE Material Rules, Annexe B): // foil lockout 300 ms +/-25 minimum break of contact 14 ms +/-1 // epee lockout 45 ms +/-5 minimum contact 2 ms // sabre lockout 170 ms +/-10 minimum contact 0.1-1 ms // whip-over suppressed between 4 and 15 ms of blade-to-blade contact // --------------------------------------------------------------------------- #ifndef SCORING_LOGIC_H #define SCORING_LOGIC_H #include enum Weapon : uint8_t { FOIL = 0, EPEE = 1, SABRE = 2 }; // ADC thresholds, 12 bit, 3.3 V rail, 1k pull network. // A line pulled to the rail through 1k and joined to a node pulled to ground // through 1k sits at half rail; each extra pulldown on the node drags it one // divider step lower. The worst realistic case is sabre, where B and C are // commoned at both weapons: drive + own C + opponent's B, C and lamé is one // pull-up against four pulldowns = 1/5 rail = ~819 counts, and a piste graze // makes it 1/6 = ~683. LOW_MAX sits below both so a whip-over pile-up still // reads as joined; an untouched sense line reads near zero, far below it. static const uint16_t LEVEL_LOW_MAX = 600; static const uint16_t LEVEL_HIGH_MIN = 3300; enum Level : uint8_t { LVL_LOW, LVL_MID, LVL_HIGH }; inline Level levelOf(uint16_t v) { if (v < LEVEL_LOW_MAX) return LVL_LOW; if (v >= LEVEL_HIGH_MIN) return LVL_HIGH; return LVL_MID; } // Two nodes are joined when both sit in the mid band at the same instant. // Correlation rather than absolute value, which is what makes this robust to // contact resistance: grot in the contact moves both readings together. inline bool joined(uint16_t a, uint16_t b) { return levelOf(a) == LVL_MID && levelOf(b) == LVL_MID; } // What one fencer's weapon line is touching, sampled during that fencer's // drive phase. struct Contacts { bool toOwnGround; // W joined own C - foil point at rest bool toOwnLame; // W joined own A - epee point closed, or foil self hit bool toOppLame; // W joined opponent A - the valid target bool toOppGround; // W joined opponent C - their guard bool toOppWeapon; // W joined opponent B - blade to blade bool toPiste; // W joined the piste bool floating; // W at rail, touching nothing conductive void clear() { toOwnGround = toOwnLame = toOppLame = false; toOppGround = toOppWeapon = toPiste = floating = false; } }; struct Frame { Contacts a; Contacts b; uint32_t t_us; }; struct Lights { bool onA, offA, onB, offB; bool faultA, faultB; bool signalled; // lockout resolved, drive the lamps and buzzer now }; class ScoringBox { public: ScoringBox() { setWeapon(EPEE); } void setWeapon(Weapon w) { weapon_ = w; reset(); } Weapon weapon() const { return weapon_; } void reset(); Lights update(const Frame& f); // exposed for the test harness bool hitOnA() const { return f_[0].hitOn; } bool hitOffA() const { return f_[0].hitOff; } bool hitOnB() const { return f_[1].hitOn; } bool hitOffB() const { return f_[1].hitOff; } private: enum Cand : uint8_t { CAND_NONE, CAND_ON, CAND_OFF }; struct FencerState { bool hitOn, hitOff; Cand cand; uint32_t candStart; uint32_t hitTime; bool bladeContact; uint32_t bladeStart; uint32_t bladeEnd; uint32_t bladeDur; bool lameActive; // an opponent-lamé contact is in progress bool whipReject; // ...and it was judged a whip-over at its start bool faulting; uint32_t faultStart; bool fault; void clear() { hitOn = hitOff = false; cand = CAND_NONE; candStart = 0; hitTime = 0; bladeContact = false; bladeStart = bladeEnd = bladeDur = 0; lameActive = false; whipReject = false; faulting = false; faultStart = 0; fault = false; } }; Cand classify(const Contacts& c, FencerState& s, uint32_t now) const; bool faultCondition(const Contacts& c) const; Weapon weapon_; FencerState f_[2]; bool anyHit_; uint32_t firstHitTime_; bool signalled_; }; // Timing tables, microseconds, indexed by Weapon. extern const uint32_t LOCKOUT_US[3]; extern const uint32_t CONTACT_US[3]; // Sabre whip-over: a lamé contact is rejected while blade-to-blade contact has // lasted between these bounds. TAIL_US keeps rejecting for a moment after the // blades separate, because the whip lands just after the parry breaks. static const uint32_t WHIPOVER_MIN_US = 4000; static const uint32_t WHIPOVER_MAX_US = 15000; static const uint32_t WHIPOVER_TAIL_US = 2000; // How long an impossible line state must persist before it is called a fault. static const uint32_t FAULT_US = 3000000; #endif