# 1. Software Architecture
Piolín in its current Obstacle Challenge configuration
Figure 1.1. Piolín's current Obstacle Challenge platform. The software architecture is being developed from control principles already tested in earlier Piolín programs.
Piolín's software is being developed as a **layered autonomous-control system** rather than as one long sequence of timed movements. The current Obstacle Challenge program is still under development. Therefore, this document describes the **software architecture and control principles guiding the final implementation**, while using fragments of current Piolín prototype code as engineering evidence of how these ideas are already being implemented and tested. The current Obstacle Challenge hardware is: ```text Motor A → propulsion Motor B → steering S1 → Pixy2.1 S2 → LEFT Ultrasonic Sensor S3 → RIGHT Ultrasonic Sensor S4 → Color Sensor ``` The intended software pipeline is: ```text SENSORS ↓ MEASUREMENT VALIDATION ↓ PERCEPTION ↓ STATE MANAGEMENT ↓ CONTROL ARBITRATION ↓ STEERING / SPEED COMMAND ↓ MOTORS ``` This architecture separates: ```text what Piolín measures what Piolín believes is happening what Piolín is currently trying to do how the final motor command is produced ``` That separation becomes especially important during the Obstacle Challenge, where wall following, pillar avoidance, recovery, corner handling, and safety can all request different steering behavior. --- ## 1.1 Why the Software Is Layered The Obstacle Challenge controller must continuously answer different questions: ```text Is there a relevant pillar ahead? What color is it? Which side must Piolín pass? Where is the pillar in the camera image? How close is Piolín to the left wall? How close is Piolín to the right wall? Is a safety override required? Has the current pillar actually been passed? Should Piolín still avoid or begin recovery? Has a course landmark been crossed? Is parking currently allowed? ``` These questions should not all be solved inside one steering equation. Instead, Piolín separates them conceptually into: ```text PERCEPTION → understand sensor information STATE → determine the current maneuver CONTROL → calculate the desired motion ACTUATION → make Motor A and Motor B execute it ``` The current prototypes already contain several examples of this separation. --- # 1.2 Centralized Utility Functions One useful characteristic of the current code is that common mathematical operations are isolated in helper functions rather than rewritten throughout the controller. For example: ```python def clamp(value, minimum, maximum): if value < minimum: return minimum if value > maximum: return maximum return value ``` This small function appears throughout the controller whenever a value must remain inside a safe or useful range. Examples include: ```text steering limits gyro correction sensor-derived terms wall-escape steering target changes ``` This is preferable to scattering repeated limit logic throughout the main loop. Architecturally: ```text RAW CALCULATION ↓ clamp() ↓ BOUNDED COMMAND ``` The same idea will remain useful in the final Obstacle Challenge code for limiting: ```text Pixy steering corrections wall-safety corrections recovery steering target relevance values drive speeds ``` --- # 1.3 Sensor Filtering Layer The current Piolín controller already contains a short ultrasonic filtering stage. Rather than feeding every raw ultrasonic sample directly into navigation, it uses a three-value median filter. ```python def median3(values): a, b, c = values[0], values[1], values[2] if a > b: a, b = b, a if b > c: b, c = c, b if a > b: a, b = b, a return b ``` The buffer is then updated with: ```python def update_buffer(buffer, value): buffer[0] = buffer[1] buffer[1] = buffer[2] buffer[2] = value return median3(buffer) ``` This is an important architectural decision because the navigation controller receives a more stable measurement without introducing a long averaging delay. The design principle is: ```text RAW SENSOR ↓ short validation/filter ↓ CONTROL VALUE ``` rather than: ```text RAW SENSOR ↓ immediate steering reaction ``` A short median filter is especially useful for rejecting a single abnormal ultrasonic sample. At the same time, the filter remains intentionally small because excessive filtering would create: ```text smoother measurement + slower response ``` and Piolín continues traveling while the software is waiting for new data. The final Obstacle controller can retain this same philosophy for S2 and S3. --- # 1.4 Sensor Reading Is Separated from Navigation Another useful pattern in the prototype is the separation between reading a sensor and deciding what Piolín should do with the result. For example, the ultrasonic reading function first constrains the raw measurement: ```python def read_raw_us(sensor): try: d = sensor.distance() if d < 20: d = 20 if d > MAX_US_MM: d = MAX_US_MM return d except: return MAX_US_MM ``` Then another function handles the filtered pair: ```python def read_distances(): l = update_buffer( left_buffer, read_raw_us(us_left) ) r = update_buffer( right_buffer, read_raw_us(us_right) ) return float(l), float(r) ``` The important architectural principle is not the temporary variable names used in one development file. It is the separation: ```text READ HARDWARE ↓ VALIDATE VALUE ↓ FILTER VALUE ↓ RETURN MEASUREMENT ``` The navigation controller then works with the measurement rather than directly manipulating hardware communication. For the current robot, the authoritative physical interpretation remains: ```text S2 = LEFT Ultrasonic S3 = RIGHT Ultrasonic ``` --- # 1.5 Geometry Is Calculated Before Steering One of the strongest ideas in the current prototype is that Piolín first estimates its track-relative geometry and only afterward converts that geometric error into steering. The controller contains two geometric estimates: ```python x_two_us = ( TRACK_WIDTH_MM + Di - Do ) / 2.0 ``` and: ```python x_inner = ( Di + SENSOR_OFFSET_MM ) ``` It then measures whether both ultrasonic readings are geometrically coherent: ```python geometry_error = abs( (Di + Do) - EXPECTED_SUM_MM ) ``` A confidence-like weight is produced: ```python weight = 1.0 - ( geometry_error / GEOMETRY_TRANSITION_MM ) weight = clamp( weight, 0.0, 1.0 ) ``` Finally, both geometric estimates are fused: ```python x_est = ( weight * x_two_us + (1.0 - weight) * x_inner ) ``` This is an important architectural pattern. The code does not simply say: ```text left sensor too close → steer right ``` for every condition. Instead: ```text SENSOR DATA ↓ GEOMETRIC MODEL ↓ ESTIMATED POSITION ↓ CONTROL ERROR ↓ STEERING ``` For the Obstacle Challenge, the final geometric model may evolve, but the same separation remains valuable. --- # 1.6 Nonlinear Steering from Geometric Error The prototype also converts position error into steering through a dedicated function: ```python def steering_from_error(error_mm): if abs(error_mm) <= POSITION_DEADBAND_MM: return 0.0 e_cm = abs(error_mm) / 10.0 magnitude = ( 0.35 * e_cm + 0.012 * e_cm * e_cm ) magnitude = clamp( magnitude, 0.0, MAX_STEER_OFFSET ) if error_mm > 0: return magnitude else: return -magnitude ``` This represents another useful architectural choice: ```text GEOMETRIC ERROR ↓ CONTROL FUNCTION ↓ REQUESTED STEERING ``` Near the target: ```text small error → small correction ``` while larger errors can create stronger corrections. This is different from a simple binary controller such as: ```text too far left → full right ``` because strong binary corrections often create oscillation. For the Obstacle Challenge, similar nonlinear mappings can eventually be used for: ```text Pixy horizontal error wall recovery pillar approach severity ``` without making every visible obstacle immediately produce maximum steering. --- # 1.7 Steering Target Smoothing A particularly important part of the current Piolín code is that the desired steering angle does not immediately become the Motor B target. The prototype first smooths the requested target: ```python def smooth_target(target): global current_target diff = target - current_target diff = clamp(diff, -TARGET_STEP, TARGET_STEP) current_target += diff return current_target ``` This means a requested change such as: ```text 0° → 25° ``` can instead become: ```text 0 → 1.8 → 3.6 → 5.4 → ... → 25 ``` depending on the configured target step. The goal is to reduce: ```text instant steering reversals mechanical shocks rapid zig-zag controller fighting ``` This becomes extremely useful during Obstacles because Pixy detections can move significantly across the image from one frame to the next. The intended pipeline becomes: ```text PIXY / WALL / RECOVERY LOGIC ↓ requested steering ↓ smooth_target() ↓ Motor B controller ``` --- # 1.8 Motor B Has Its Own Control Layer The prototype also separates desired steering from the actual motor command. ```python def steering_to(target): error = target - steer.angle() if abs(error) <= STEER_TOLERANCE: steer.stop(Stop.BRAKE) return speed = STEER_MOTOR_KP * error speed = clamp( speed, -MAX_STEER_MOTOR_SPEED, MAX_STEER_MOTOR_SPEED ) steer.run(int(speed)) ``` This means the navigation controller does not directly command: ```text Motor B speed = fixed value ``` every time it wants to turn. Instead: ```text NAVIGATION → desired steering position ``` and: ```text STEERING CONTROLLER → moves Motor B toward that position ``` This separation is important for troubleshooting. If debug output shows: ```text requested steering = RIGHT ``` but the physical wheels move left, the failure exists in: ```text actuation / steering sign / mechanism ``` rather than: ```text Pixy classification ``` That distinction is especially valuable when diagnosing apparently inverted Red/Green obstacle behavior. --- # 1.9 Independent Wall-Safety Layer The current prototype includes a particularly important concept for the final Obstacle controller: **wall safety has its own function and can override normal navigation**. The function first determines whether either lateral wall is becoming unsafe: ```python left_danger = left_mm < WALL_SOFT_MM right_danger = right_mm < WALL_SOFT_MM if not left_danger and not right_danger: return False, 0.0, None ``` If a wall is close, the controller determines which side requires attention and calculates a progressive escape magnitude. A left-side danger ultimately produces: ```python if chosen == "LEFT": return True, STEER_RIGHT * mag, speed ``` while right-side danger produces the opposite response: ```python return True, STEER_LEFT * mag, speed ``` The main loop then gives this layer priority: ```python wall_active, wall_cmd, wall_speed = side_wall_guard( left, right ) if wall_active: combined_offset = wall_cmd ``` This is a major architectural concept. The controller does not merely add: ```text wall correction + another controller ``` in every situation. Instead, critical safety can **take control**. For the final Obstacle Challenge architecture, this idea becomes: ```text normal geometry ↓ pillar avoidance ↓ BUT ↓ if physical wall becomes critical ↓ safety override ``` The exact thresholds remain calibration values. --- # 1.10 State-Dependent Speed The prototype also shows that Motor A speed can depend on the current vehicle condition. For example: ```python if wall_active: drive.run(wall_speed) elif acquire_progress < 1.0: drive.run(ACQUIRE_SPEED) elif abs(combined_offset) >= TURN_STEER_THRESHOLD: drive.run(TURN_SPEED) else: drive.run(DRIVE_SPEED) ``` This demonstrates an important software principle: > **Speed is part of the autonomous controller, not an independent constant.** Piolín may require different speeds during: ```text normal straight driving initial acquisition strong steering critical wall recovery pillar approach pillar avoidance parking ``` The final Obstacle controller can use the same architecture: ```text STATE ↓ desired steering + desired drive speed ``` rather than running Motor A at one constant speed regardless of the maneuver. --- # 1.11 Event Confirmation from the Current Color Prototype The second Piolín prototype demonstrates another important architecture: **do not treat one sensor sample as a complete navigation event**. The code maintains: ```python candidate_color = None candidate_count = 0 ready_for_new_color = True release_count = 0 ``` When a color appears, it must remain consistent for multiple readings: ```python if detected == candidate_color: candidate_count += 1 else: candidate_color = detected candidate_count = 1 ``` Only then is the event accepted: ```python if candidate_count >= COLOR_CONFIRMATIONS: ``` This creates: ```text RAW DETECTION ↓ CANDIDATE ↓ CONFIRMATION ↓ ACCEPTED EVENT ``` The same architecture is extremely useful for Pixy2.1. A future obstacle version can conceptually use: ```text possible Red ↓ Red remains plausible ↓ confirmed Red target ↓ begin pass-right maneuver ``` instead of: ```text one Red sample → immediate maximum steering ``` --- # 1.12 Event Latching and Re-Arming The same color prototype also prevents one physical line from being counted repeatedly. After one accepted color event: ```python ready_for_new_color = False release_count = 0 candidate_color = None candidate_count = 0 ``` The detector then waits until the robot has clearly left the color: ```python if detected is None: release_count += 1 if release_count >= COLOR_RELEASE_CONFIRMATIONS: ready_for_new_color = True release_count = 0 ``` Architecturally: ```text DETECT ↓ CONFIRM ↓ ACCEPT ↓ LOCK ↓ WAIT FOR PHYSICAL RELEASE ↓ RE-ARM ``` This is one of the most transferable patterns from the current program to obstacle vision. The Pixy equivalent is: ```text candidate pillar ↓ confirmed target ↓ lock target ↓ avoid pillar ↓ confirm physical clearance ↓ release target ↓ allow next target ``` This prevents unstable behavior when multiple pillars appear in the camera. --- # 1.13 Current Obstacle Perception Layer
Pixy2.1 installed on Piolín for obstacle perception
Figure 1.6. Pixy2.1 becomes the primary forward-perception device during the Obstacle Challenge.
The final Obstacle Challenge controller will replace the Open-specific S1 Gyro logic with Pixy2.1 perception. The camera provides block information such as: ```text signature x y width height ``` The current signature mapping is: ```text sig1 → Pink → parking reference sig2 → Red → pass RIGHT sig3 → Green → pass LEFT ``` The architecture must keep two ideas separate: ```text SIGNATURE → WHAT the pillar is → which side must be passed ``` and: ```text x / y / width / height → WHERE the target appears → how visually relevant it is ``` Therefore: ```text RED → RIGHT ``` and: ```text GREEN → LEFT ``` remain fixed regardless of the target's image position. --- # 1.14 Target Selection Before Avoidance Pixy may provide more than one block. The final architecture should therefore avoid directly doing: ```text blocks[0] → steering ``` Instead: ```text READ PIXY ↓ VALIDATE BLOCKS ↓ FILTER ALLOWED SIGNATURES ↓ COMPARE RELEVANCE ↓ SELECT TARGET ↓ TARGET LOCK ``` A target-selection layer can eventually consider: ```text signature x y width height previous target current state ``` The final scoring formula is not yet fixed. This is intentional. The architecture defines **where target selection belongs** without pretending the current experimental weighting is final. --- # 1.15 State Machine The existing code already contains early forms of explicit state behavior such as: ```text ACQUIRE NORMAL PARKING ``` and the color prototype contains: ```text READY FOR NEW COLOR WAITING FOR RELEASE ``` The final Obstacle controller expands this idea. A practical state architecture is: ```text START ↓ ACQUIRE ↓ NORMAL ↓ TARGET_ACQUIRE ↓ AVOID ↓ PASS_CONFIRM ↓ RECOVER ↓ NORMAL ``` with additional states such as: ```text CORNER PARKING STOP ``` Each state answers one question: > **What is Piolín currently trying to accomplish?** That is much easier to reason about than allowing every controller to act simultaneously. --- # 1.16 Control Arbitration One of the main reasons for a state machine is to decide which controller should have authority. Imagine: ```text Pixy: Red pillar → move right ``` while: ```text normal wall controller: move left toward center ``` If both commands are simply added: ```text right + left → cancellation → weak avoidance ``` This can make a correct pillar detector appear broken. The intended architecture is therefore approximately: | Priority | Controller | | :---: | :--- | | 1 | Critical wall safety | | 2 | Active pillar avoidance | | 3 | Active corner handling | | 4 | Post-pillar recovery | | 5 | Normal geometry control | This is conceptually consistent with the prototype's existing pattern: ```python if wall_active: combined_offset = wall_cmd ``` where safety explicitly overrides the normal combined command. The final controller should always produce **one clear final steering request**. --- # 1.17 Pillar-Pass Confirmation A camera target disappearing is not sufficient evidence that the pillar has physically been passed. Piolín turns the camera whenever the chassis turns. Therefore: ```text Pixy target disappeared ``` may mean: ```text camera rotated away from pillar ``` rather than: ```text pillar is behind vehicle ``` The intended state transition is: ```text AVOID ↓ visual target becomes peripheral / lost ↓ PASS_CONFIRM ↓ evaluate S2/S3 geometry ↓ pillar clearance confirmed ↓ RECOVER ``` This is directly related to the event-release architecture already used in the color prototype: ```text do not accept a new event until the previous physical condition has been released ``` For Obstacles: ```text do not acquire a new pillar until the current pillar has been physically cleared ``` --- # 1.18 Recovery Is Its Own Controller
Piolín performing a red obstacle maneuver
Figure 1.7. Obstacle navigation must continue after the pillar is cleared; recovery is treated as a separate control phase.
The complete obstacle maneuver is: ```text DETECT ↓ SELECT ↓ LOCK ↓ AVOID ↓ PASS ↓ COUNTERSTEER ↓ RECOVER ``` Recovery should not be hidden inside the pillar avoidance equation. It has a different objective. During avoidance: ```text move to required side of pillar ``` During recovery: ```text return toward useful course geometry ``` This makes it possible to tune: ```text pillar clearance ``` independently from: ```text post-pillar recentering ``` If Piolín correctly passes a Red pillar but later hits the wall, the engineering problem can then be identified as: ```text RECOVERY ``` rather than weakening the successful Red avoidance maneuver. --- # 1.19 Course Events Remain Independent The Color Sensor on S4 should remain a separate course-state system. Its job is not to influence Pixy classification directly. The architecture is: ```text PIXY2.1 → forward objects ``` ```text S2 / S3 → lateral geometry ``` ```text S4 → floor landmarks ``` The event-confirmation logic already tested in the current color program can continue to prevent duplicate floor events. Those events can later update: ```text course progress corner count lap state parking permission ``` without directly commanding obstacle steering. --- # 1.20 Diagnostics Are Part of the Controller The current large prototype already provides detailed telemetry such as: ```python print( "MODE:", "ACQUIRE" if acquire_progress < 1.0 else "NORMAL", "LINES:", line_count, "L:", int(left), "R:", int(right), "X:", round(x_est, 1), "ERR:", round(error, 1), "GY_ERR:", round(gyro_error, 1), "WALL_SAFE:", wall_active, "CMD:", round(combined_offset, 1), "TARGET:", round(current_target, 1), "REAL:", round(steer.angle(), 1) ) ``` This is valuable because it records several layers simultaneously: ```text current state sensor information estimated geometry controller output requested steering actual steering position ``` The Obstacle controller should keep a similarly useful diagnostic structure. A future Obstacle debug line could logically expose: ```text STATE TARGET SIGNATURE X Y WIDTH HEIGHT S2 S3 FINAL STEERING COMMAND ``` This lets the team determine whether a failure occurred in: ```text camera detection target selection state controller arbitration Motor B response ``` rather than guessing from the final collision. --- # 1.21 Existing Code vs. Final Obstacle Code The existing Piolín programs are **development references**, not final Obstacle Challenge source code. Some current prototype features are Open-specific or temporary, including: ```text gyro heading control manual test direction working wall targets temporary parking constants timed curve experiments prototype line counting development sensor mappings ``` These details should not automatically be copied into the final Obstacle program. What is being preserved is the underlying software architecture. | Existing Code Pattern | Final Obstacle Use | | :--- | :--- | | `clamp()` | Keep controller values bounded | | `median3()` | Short ultrasonic filtering | | `read_distances()` | Sensor abstraction | | Geometric fusion | Track-relative geometry | | `steering_from_error()` | Error-to-steering conversion | | `smooth_target()` | Steering rate limiting | | `steering_to()` | Motor B position control | | `side_wall_guard()` | Independent wall safety | | State-dependent speed | Maneuver-specific propulsion | | Candidate confirmation | Pixy target confirmation | | Event latch | Target lock | | Release confirmation | Pillar-pass confirmation | | Debug print | Controller telemetry | This allows the code to evolve without discarding control principles already tested on Piolín. --- # 1.22 Planned Program Structure During fast competition development, one structured source file can still be practical. As the final controller becomes stable, responsibilities can be separated conceptually into: ```text src/ │ ├── obstacle_challenge.py │ ├── sensors/ │ ├── pixy.py │ ├── ultrasonic.py │ └── color.py │ ├── control/ │ ├── steering.py │ ├── drive.py │ ├── walls.py │ └── obstacles.py │ └── navigation/ ├── state_machine.py └── target_selection.py ``` This is a possible architectural direction, not a requirement to create unnecessary empty modules. The important part is that responsibilities remain separated logically. A single file can still implement good architecture if functions and state boundaries remain clear. --- # 1.23 Complete Intended Control Cycle The final Obstacle software cycle can be represented as: ```text while robot_is_running: READ SENSORS │ ├── Pixy blocks ├── S2 left distance ├── S3 right distance └── S4 floor state │ ▼ VALIDATE / FILTER │ ▼ UPDATE PERCEPTION │ ├── target candidate ├── wall geometry └── floor event │ ▼ UPDATE STATE │ ├── NORMAL ├── TARGET_ACQUIRE ├── AVOID ├── PASS_CONFIRM ├── RECOVER ├── CORNER └── PARKING │ ▼ CALCULATE CONTROL │ ├── obstacle command ├── wall command ├── safety command └── recovery command │ ▼ ARBITRATE PRIORITY │ ▼ SMOOTH FINAL STEERING │ ▼ COMMAND MOTOR B │ ▼ SELECT DRIVE SPEED │ ▼ COMMAND MOTOR A │ ▼ OUTPUT DEBUG DATA ``` This is the architecture toward which the current experimental programs are evolving. --- # 1.24 Current Development Status The following ideas are already represented directly in Piolín's current development code: ```text sensor abstraction short filtering geometry estimation bounded controllers smooth steering targets separate Motor B control wall-safety override state-dependent speed candidate confirmation event locking release conditions parking state diagnostic telemetry ``` The main Obstacle-specific areas that still require development and validation are: ```text Pixy block parsing final target relevance model multiple-target selection temporary target locking target-loss tolerance pillar-pass confirmation Red trajectory Green trajectory wall/obstacle control arbitration post-pillar recovery corner integration final parking ``` The repository should therefore distinguish between: ```text ARCHITECTURE ALREADY SUPPORTED BY PROTOTYPES ``` and: ```text FINAL ALGORITHM STILL UNDER DEVELOPMENT ``` --- # 1.25 Final Engineering Assessment Piolín's software development is moving from several experimental controllers toward one coherent **state-based and layered autonomous architecture**. The current programs already demonstrate important engineering concepts directly in Python: ```text short sensor filtering geometric estimation bounded nonlinear corrections steering target smoothing closed-loop Motor B control independent wall-safety overrides state-dependent speed sensor-event confirmation event latching controlled release conditions diagnostic telemetry ``` These concepts provide a foundation for the final Obstacle Challenge controller. The intended information flow is: ```text Pixy2.1 → obstacle identity and visual geometry S2/S3 → physical lateral geometry S4 → course events ``` followed by: ```text PERCEPTION → STATE → CONTROL ARBITRATION → ACTUATION ``` The camera should determine: ```text WHAT pillar is relevant ``` but not directly control the steering motor. The state machine should determine: ```text WHAT Piolín is currently trying to accomplish ``` and the control layer should determine: ```text HOW to execute that objective safely ``` before Motor B receives one final steering target. For obstacle rules: ```text RED → PASS RIGHT GREEN → PASS LEFT ``` remains permanent. Pixy image coordinates influence target geometry and steering magnitude, not the required passing side. Because the uploaded programs remain development versions, temporary constants and Open-specific logic are treated as evidence of the architecture rather than as final Obstacle Challenge specifications. The main software principle is: > **Read and validate the sensors first, interpret the environment second, choose one navigation state third, resolve controller priorities fourth, and only then send a final command to the motors.** This structure allows PiolínTech to improve target selection, camera processing, wall safety, recovery, or steering independently without rebuilding the entire autonomous controller each time one subsystem changes. ---
### [← Back to PiolínTech Main README](../../README.md)