// --------------------------------------------------------------------------- // Fencing scoring box - hit simulator // // Arduino Nano. Fakes fencing touches of exactly known duration so you can // verify the box against the FIE numbers instead of guessing. A human cannot // produce a repeatable 13 ms contact; this can, to the microsecond. // // How it works // ------------ // Every input on the box is held at one rail by its own 1k resistor. A touch // simply joins two of those nodes, which lands both at mid rail. To fake that, // pull each node toward mid rail from the other side through 2.2k: // // weapon nodes are pulled UP by the box -> simulator drives them LOW // all others are pulled DOWN by the box -> simulator drives them HIGH // // Pins are left as INPUT when idle so they are invisible to the box. // // IMPORTANT // --------- // The Nano runs at 5 V and the F401 analog pins are not 5 V tolerant. The // 2.2k series resistors are what make this safe: a node pulled "high" reaches // 5 * 1k/(1k+2.2k) = 1.56 V, and the clamp current if anything goes wrong is // under 1 mA. Do not reduce them, and do not drive a weapon node high. // // Put the box in TEST MODE (hold its mode button while powering up) so the // drive phase stays frozen on fencer A. // // Wiring, each through its own 2.2k: // D2 -> fencer A weapon node (B line) // D3 -> fencer A lame node (A line) // D4 -> fencer A ground node (C line) // D5 -> fencer B lame node // D6 -> fencer B ground node // D7 -> piste node // D8 -> fencer B weapon node // GND -> box GND (essential, shared reference) // // Also connect a logic analyzer to the box lamp pins and to D9, which pulses // at the exact start of every simulated contact as a trigger. // --------------------------------------------------------------------------- const uint8_t A_WEAPON = 2; // drive LOW to engage const uint8_t A_LAME = 3; // drive HIGH to engage const uint8_t A_GROUND = 4; // drive HIGH const uint8_t B_LAME = 5; // drive HIGH const uint8_t B_GROUND = 6; // drive HIGH const uint8_t PISTE = 7; // drive HIGH const uint8_t B_WEAPON = 8; // drive LOW const uint8_t TRIGGER = 9; // idle = high impedance, invisible to the box static inline void release(uint8_t pin) { pinMode(pin, INPUT); } static inline void engage(uint8_t pin, bool high) { pinMode(pin, OUTPUT); digitalWrite(pin, high ? HIGH : LOW); } static void releaseAll() { for (uint8_t p : {A_WEAPON, A_LAME, A_GROUND, B_LAME, B_GROUND, PISTE, B_WEAPON}) release(p); } // Hold a set of nodes joined for exactly `us` microseconds. // `weaponLow` engages the A weapon line; the others are the partners. static void touch(uint32_t us, bool weapon, bool aLame, bool aGround, bool bLame, bool bGround, bool piste, bool bWeapon) { digitalWrite(TRIGGER, HIGH); if (weapon) engage(A_WEAPON, false); if (aLame) engage(A_LAME, true); if (aGround) engage(A_GROUND, true); if (bLame) engage(B_LAME, true); if (bGround) engage(B_GROUND, true); if (piste) engage(PISTE, true); if (bWeapon) engage(B_WEAPON, true); // high so it reads mid, not rail if (us > 16000) delay(us / 1000); else delayMicroseconds(us); releaseAll(); digitalWrite(TRIGGER, LOW); } // Foil rests with the point closed: weapon joined to its own ground line. static void foilRest(uint32_t ms) { engage(A_WEAPON, false); engage(A_GROUND, true); delay(ms); releaseAll(); } // A real sabre rests the same way, permanently: FIE m.24.4 requires both // bodywire sockets to be in direct contact with the guard, so B and C are // commoned whenever a healthy sabre is plugged in. Every sabre simulation // must therefore hold the weapon-to-own-ground join for its whole duration. static void sabreRest(uint32_t ms) { foilRest(ms); } struct Test { const char* name; void (*run)(); }; static void t_foil_13ms() { foilRest(200); touch(13000, true,false,false, true,false,false,false); foilRest(600); } static void t_foil_16ms() { foilRest(200); touch(16000, true,false,false, true,false,false,false); foilRest(600); } static void t_foil_floor() { foilRest(200); touch(60000, true,false,false, false,false,true,false); foilRest(600); } static void t_foil_offt() { foilRest(200); touch(20000, true,false,false, false,false,false,false); foilRest(600); } static void t_epee_1ms() { delay(200); touch( 1000, true,true,false, false,false,false,false); delay(300); } static void t_epee_4ms() { delay(200); touch( 4000, true,true,false, false,false,false,false); delay(300); } static void t_epee_floor() { delay(200); touch(40000, true,true,false, false,false,true,false); delay(300); } // All sabre touches keep aGround engaged: that is the commoned B-C resting // state of a healthy sabre (see sabreRest above). Blade-to-blade contact also // engages B's ground, because the opponent's B and C are one conductor too. static void t_sabre_rest() { sabreRest(3000); } static void t_sabre_hit() { sabreRest(200); touch( 1200, true,false,true, true,false,false,false); sabreRest(400); } static void t_sabre_floor(){ sabreRest(200); touch(40000, true,false,true, false,false,true,false); sabreRest(400); } static void t_sabre_whip() { sabreRest(200); touch(8000, true,false,true, false,true,false,true); // 8 ms blade to blade touch(3000, true,false,true, true,true,false,true); // whips onto the lame, blades still touching sabreRest(400); } static void t_sabre_parry_then_hit() { sabreRest(200); touch(25000, true,false,true, false,true,false,true); // 25 ms, outside window touch(3000, true,false,true, true,true,false,true); sabreRest(400); } // Two fencers, B landing `gapUs` after A. Use to measure the real lockout. static void t_double(uint32_t gapUs) { delay(200); engage(A_WEAPON, false); engage(A_LAME, true); delayMicroseconds(4000); releaseAll(); delayMicroseconds(gapUs); engage(B_WEAPON, false); engage(B_LAME, true); delayMicroseconds(4000); releaseAll(); delay(400); } static void t_double_30ms() { t_double(30000); } static void t_double_60ms() { t_double(60000); } static const Test FOIL_TESTS[] = { {"foil 13 ms on target -> expect NO light", t_foil_13ms}, {"foil 16 ms on target -> expect GREEN", t_foil_16ms}, {"foil floor hit -> expect NO light", t_foil_floor}, {"foil off target -> expect WHITE", t_foil_offt}, }; static const Test EPEE_TESTS[] = { {"epee 1 ms -> expect NO light", t_epee_1ms}, {"epee 4 ms -> expect GREEN", t_epee_4ms}, {"epee floor hit -> expect NO light", t_epee_floor}, {"epee double at 30 ms -> expect BOTH", t_double_30ms}, {"epee single at 60 ms -> expect GREEN only", t_double_60ms}, }; static const Test SABRE_TESTS[] = { {"sabre at rest 3 s -> expect NO light, NO fault", t_sabre_rest}, {"sabre 1.2 ms -> expect GREEN", t_sabre_hit}, {"sabre floor hit -> expect NO light", t_sabre_floor}, {"sabre whip-over 8 ms -> expect NO light", t_sabre_whip}, {"sabre parry 25 ms -> expect GREEN", t_sabre_parry_then_hit}, }; void setup() { Serial.begin(115200); pinMode(TRIGGER, OUTPUT); digitalWrite(TRIGGER, LOW); releaseAll(); delay(1500); Serial.println(F("# hit simulator ready")); Serial.println(F("# send f, e or s to run that weapon's suite")); } static void runSuite(const Test* t, size_t n, const char* label) { Serial.print(F("\n=== ")); Serial.print(label); Serial.println(F(" ===")); for (size_t i = 0; i < n; ++i) { Serial.print(F(" ")); Serial.println(t[i].name); t[i].run(); delay(3500); // let the box finish its light period } Serial.println(F("=== done ===\n")); } void loop() { if (!Serial.available()) return; char c = Serial.read(); if (c == 'f') runSuite(FOIL_TESTS, sizeof FOIL_TESTS / sizeof(Test), "FOIL"); if (c == 'e') runSuite(EPEE_TESTS, sizeof EPEE_TESTS / sizeof(Test), "EPEE"); if (c == 's') runSuite(SABRE_TESTS, sizeof SABRE_TESTS / sizeof(Test), "SABRE"); }