// --------------------------------------------------------------------------- // Fencing ohm tester - portable measurement and verdict logic // // Free of Arduino dependencies so it compiles and unit tests on a PC, like // the scoring box's scoring_logic. The sketch owns the muxes and the ADC and // hands this code resistances; everything here is arithmetic and verdicts. // // Limits (FIE Material Rules, August 2026): // body cord, each wire 1 ohm m.29.1c, m.31.1b // foil and epee weapon 2 ohm m.5.4a // sabre weapon 1 ohm m.5.4a // lame, any two points 5 ohm m.28.5a (500 g probe) // sabre mask, clip to mesh 5 ohm m.32.5 // mask wire, clip to clip 1 ohm m.27.1, m.32 // --------------------------------------------------------------------------- #ifndef OHM_LOGIC_H #define OHM_LOGIC_H #include // Terminal = mux channel. The same number selects it on all four muxes. enum Term : uint8_t { T_J1A = 0, T_J1B, T_J1C, // top socket: reel end, or the cord carrying a weapon T_J2A, T_J2B, T_J2C, // front socket: weapon end of the cord under test T_CLIP, // clip post: a cord's alligator clip, a mask wire T_PROBE, // probe jack: weighted lame probe T_COUNT }; static const float LIM_CORD = 1.0f; static const float LIM_FOIL = 2.0f; static const float LIM_EPEE = 2.0f; static const float LIM_SABRE = 1.0f; static const float LIM_LAME = 5.0f; static const float LIM_WIRE = 1.0f; // A path at or below CLOSED_MAX is a conductor (maybe a bad one). Between // CLOSED_MAX and SHORT_MAX it is a short when insulation is expected, between // SHORT_MAX and LEAK_MAX it is leaky (damp, dirt, worn insulation), above // LEAK_MAX it is open. static const float CLOSED_MAX = 50.0f; static const float SHORT_MAX = 1000.0f; static const float LEAK_MAX = 100000.0f; static const float R_OPEN = 1.0e9f; // what "no current at all" reads as // ------------------------------------------------------------ ADC to ohms // One ratiometric reading. Counts are ADS1115 16-bit two's complement; fsr is // the full-scale range in volts for that conversion (0.256, 4.096...). The // *Off values are the same conversions with the drive current switched off. struct RawReading { int32_t xy, xyOff; float xyFsr; // across the item: AIN0 - AIN1 int32_t ref, refOff; float refFsr; // across R_REF: AIN2 - AIN3 }; // Below this many counts of reference voltage (on the 0.256 V range that is // about 3 uA of test current) there is no measurable current: open. static const int32_t MIN_REF_COUNTS = 4; float ratioToOhms(const RawReading& r, float rRef); // ------------------------------------------------------------- verdicts enum Verdict : uint8_t { V_PASS, // conductor within its limit, or insulation intact V_HIGH, // conducts but over the limit V_OPEN, // should conduct and does not V_SHORT, // should be insulated, under 1 kohm V_LEAKY, // should be insulated, 1 k to 100 k V_WAIT // not enough seen yet (weapon modes) }; // A path that should conduct, against its limit. Verdict conductorVerdict(float ohms, float limit); // A path that should be insulated. Verdict insulationVerdict(float ohms); const char* verdictWord(Verdict v); // Short, fixed-width text for a resistance: "0.214", "12.3", "512", "12k", "OPEN". void formatOhms(float ohms, char* out, int outSize); // ------------------------------------------------------- the meter seam // Two-terminal resistance between terminals, current driven x -> y. The // sketch implements it with the muxes and the ADS1115; tests implement it // with a resistor network. class Meter { public: virtual float ohms(Term x, Term y) = 0; virtual ~Meter() {} }; // ----------------------------------------------------------- cord test enum CordType : uint8_t { CORD_NONE, // nothing (or nothing that conducts) between the sockets CORD_3PIN, // three prongs at both ends: line A arrives at J2-A (epee cord) CORD_CLIP // line A arrives at the clip: foil / sabre cord, 2-prong at J2 }; struct WireResult { float ohms; // plug to plug (or plug to clip) resistance, R_OPEN if open Term farEnd; // where it arrived bool crossed; // arrived at the wrong pin of J2 Verdict v; }; struct CordReport { CordType type; WireResult wire[3]; // A, B, C float shortOhms[3]; // A-B, B-C, A-C measured at J1 Verdict shortV[3]; bool pass; }; CordReport testCord(Meter& m); // The last cord that tested continuous on all three wires. Weapon and lame // readings subtract it so they show the weapon alone. struct CordMemory { bool valid = false; float a = 0, b = 0, c = 0; void remember(const CordReport& r); }; // -------------------------------------------------------- weapon tests // Each update() takes the raw J1 readings of one pass; the cord is // subtracted inside. They latch what they have seen so a quick tip press // is not missed, and reset() starts over. struct FoilSabreTest { // At rest a foil and a sabre look the same: B to C closed (through the // foil's point, or the sabre's guard, m.24.4). A tip press that opens it // proves a foil. Until then the reading is judged against both limits: // up to 1 ohm passes either weapon, 1 to 2 ohm passes a foil only. bool seenRest = false, seenOpen = false; bool closedNow = false; float restOhms = 0; // weapon alone, latest at-rest reading float worstRest = 0; // highest at-rest reading seen bool partial = false; // seen 50 ohm to 100 k: dirty point or leak uint8_t partialRun = 0; bool seenProbe = false; // sabre: probe on the blade, C to probe bool probeNow = false; float probeOhms = 0; float worstProbe = 0; uint16_t presses = 0; // tip presses seen (B-C opening) void reset() { *this = FoilSabreTest(); } void update(float rBC, float rCprobe, const CordMemory& cord); }; struct EpeeTest { // Epee is normally open: A to B closes only when the tip is pressed, // and neither tip wire may touch the guard (C). bool seenOpenRest = false, seenPressed = false; bool closedNow = false; float pressedOhms = 0; // weapon alone, latest pressed reading float worstPressed = 0; float minGuard = R_OPEN; // lowest A-C or B-C seen: guard insulation uint16_t presses = 0; void reset() { *this = EpeeTest(); } void update(float rAB, float rAC, float rBC, const CordMemory& cord); Verdict verdict() const; Verdict guardVerdict() const { return insulationVerdict(minGuard); } }; struct LameTest { // Probe slid over the lame (or mask); the clip is on the cord at J1 or // on the clip post. Tracks the worst contact seen. float liveOhms = R_OPEN; float prevOhms = R_OPEN; float worst = 0; bool seenContact = false; float limit = LIM_LAME; void reset() { float l = limit; *this = LameTest(); limit = l; } void update(float rProbeClip, float rProbeJ1A, const CordMemory& cord); Verdict verdict() const; }; // ------------------------------------------------------------ flex test // Fed by the fast dropout sampler: one sample per wire per pass, true when // test current flows. A break is two or more consecutive dead samples. struct LineFlex { uint32_t breaks = 0; uint32_t longestUs = 0; bool everClosed = false; bool openNow = false; uint8_t deadRun = 0; uint32_t deadSinceUs = 0; }; struct FlexTracker { LineFlex line[3]; void reset() { *this = FlexTracker(); } void sample(int ln, bool conducting, uint32_t tUs); uint32_t totalBreaks() const { return line[0].breaks + line[1].breaks + line[2].breaks; } }; // ------------------------------------------------------------ auto mode // No modes to pick: the tester works out what is plugged in from one scan // of the terminals and runs the matching test. Its answer is a traffic // light and a sound, so nobody has to read numbers with their hands full. enum Light : uint8_t { L_OFF, L_GREEN, L_YELLOW, L_RED }; enum Sound : uint8_t { SND_NONE, SND_TICK, // a tip press registered (like a scoring box) SND_BREAK, // a break while flexing a cord SND_PASS, // the light just turned green SND_FAIL // the light just turned red }; enum Situation : uint8_t { S_IDLE, // nothing connected (or an epee at rest, until its tip is pressed) S_CORD, // a body cord in both sockets S_FOILSABRE, // B-C closed through a cord in J1: foil at rest, or a sabre S_EPEE, // A-B closed through a cord in J1: an epee tip was pressed S_PROBE, // the probe is touching something wired to the clip or the J1 cord S_ODD // A-C closed alone: a short to the guard, most likely }; // One pass over the terminal pairs auto mode needs (about 110 ms). struct Scan { float j1j2[3]; // J1-x to J2-x, x = A, B, C float aClip; // J1-A to CLIP float ab, bc, ac; // between J1's own pins float probeClip, probeA, probeC; }; Scan scanAll(Meter& m); // How long "nothing connected" lasts before the next thing plugged in is // treated as new. Short for weapons (an epee rests open), long for the probe // (it is lifted between strokes). static const uint32_t FRESH_MS = 2500; static const uint32_t FRESH_PROBE_MS = 10000; // Flex counting starts this long after all three cord wires first conduct, // so the plug going in is not counted as breaks. static const uint32_t FLEX_ARM_MS = 500; struct AutoTester { Situation sit = S_IDLE; Situation shown = S_IDLE; // what the screen shows: the last real situation Light light = L_OFF; bool blink = false; // yellow blinking = do the next step Sound sound = SND_NONE; // event from the latest update, cleared each call // cord session CordReport cord{}; bool cordFull = false; // all three wires have conducted on their own pins uint32_t cordFullSinceMs = 0; float worstWire[3] = {0, 0, 0}; float minShort[3] = {R_OPEN, R_OPEN, R_OPEN}; uint8_t openRun[3] = {0, 0, 0}; // consecutive passes a wire has read open uint8_t noneRun = 0; uint32_t breaks = 0; FoilSabreTest fs; EpeeTest ep; LameTest probe; uint32_t lastSeenMs = 0; // last pass where something was connected bool lastAbOpen = false; // A-B open on the previous pass (epee at rest) void reset(); // Not in a cord session: classify one scan and run the matching test. void update(const Scan& s, const CordMemory& mem, uint32_t nowMs); // In a cord session: the full cord report replaces the scan. void updateCord(const CordReport& r, CordMemory& mem, uint32_t nowMs); // Breaks counted by the fast sampler since the cord was armed. void noteBreaks(uint32_t total); bool flexArmed(uint32_t nowMs) const { return sit == S_CORD && cordFull && nowMs - cordFullSinceMs >= FLEX_ARM_MS; } Verdict wireVerdict(int i) const; Verdict shortVerdict(int i) const { return insulationVerdict(minShort[i]); } private: void enter(Situation s, uint32_t nowMs); void setLight(Light l, bool blinkIt = false); }; #endif