• Keypad PCB Layout and Camera

    Apollo Timbersa day ago 0 comments

    When I was looking for switches I knew I wanted some quality momentary ones, so I went with the UB15SKG03N-G shown below, As it is a though hole Device and where most of the quality feel for the use will be. So I took the time to whip up a PCB to mount them to the case. If my measurements are right they should line up perfect. I added the piezo speaker too as it had room... 

    Next up to add a additional feature and get the Coral dev board mini doing something, I added a camera to the back of it and the Coral will run something like MobileNetV3 image classification.  

    To classify a image it would go something like this---  User selects menu button, scrolls down one, hits enter, the proc reads in the button/menu section, It brings the Coral out of idle and tells it to take a image, at the same time the microprocessor is reading in ambient light and turning on the two bright white LED's near the camera to help if needed, a image is taken, the AI does inference in 10ms or so and send the message back to the Pico to display on the screen (“Possibly: Sugar Maple — Confidence: 89%”). Issue at least for now is the Coral can indeed same image to the SD card however I will not be displaying it on screen. 

    With the addition of the super bright white LED's a easy value add is a simple selection to turn them on/off for a flashlight if needed. 

    Going to cover with a 16mm x 1mm sapphire watch glass as well. hence the larger round cutout.

  • Compass Progress & Anomaly Detector

    Apollo Timbers2 days ago 0 comments

    The Bruised Compass

    I recently combined two Adafruit sensors—the TLV493D magnetometer and the LIS3DH accelerometer, to build a digital compass with tilt compensation. I secured them together with a Velcro cable tie (very professional) so the accelerometer stays aligned during calibration. While some call this “sensor fusion,” here it simply means combining data from both sensors to get a reliable heading.

    Getting it to work took some troubleshooting, especially with heading jumps and interference indoors. Magnetometers are *very* sensitive to electrical noise, so outdoor testing and a properly isolated case are next on my list. I finally added some smoothing but also a way to reject large jumps... the code is tested and probably as stable as you can get indoors...

    In hindsight, using an integrated IMU like the Adafruit BNO085 would’ve made this process much smoother, since it handles sensor fusion and tilt compensation internally. I plan to upgrade to this chip for the next iteration.

    Now for some fun code... *untested*

    The Anomaly Detector

    For the next phase, I decided, why stop at practical features? So I built a multinet anomaly detector... It doesn’t just warn you about boring stuff like sudden movement or a weird tilt. it’s also on the lookout for “high strangeness.” We’re talking time hiccups, gravity doing a backflip, compass spins worthy of a paranormal show, or sudden stillness so perfect you might wonder if you just glitched into a simulation or got stuck in a stasis field but for some reason the analyzer is still working...

    Some of these nets are dead serious (catching real hazards and sensor issues). Others? Let’s just say they’re designed to flag the impossible, the improbable, and anything else that would get Mulder and Scully interested. Either way, the whole point is that you get plain English explanations.

    The compass code has been tested the anom code will need to wait till the IMU arrives. (also not sure it can ever be fully tested) 

    AI Field Analyzer on GitHub

    https://github.com/thedocdoc/AI-Field-Analyzer/tree/main

  • Geiger Counter Online

    Apollo Timbers4 days ago 0 comments

    Had some older C++ code that I was able to get converted to CircuitPython. Ran into a few issues first it was not detecting pulses, then it dawned on me the reason back in the day I went with C++ is it had code interrupts, though CircuitPython does not. So I had to basically optimize the code to update the screen less, every 3 seconds and also read the +CO2 and VOX sensor. 

    Current output

    CO₂: 533 ppm | TVOC: 0 ppb | CPM: 5 | uSv/h: 0.0942827 | ✅ Excellent air quality. | ✅ Air is clean. | ✅ Background radiation—no risk.Pulse detected! Total: 2
    CO₂: 535 ppm | TVOC: 0 ppb | CPM: 5 | uSv/h: 0.0942827 | ✅ Excellent air quality. | ✅ Air is clean. | ✅ Background radiation—no risk.Pulse detected! Total: 3
    CO₂: 551 ppm | TVOC: 0 ppb | CPM: 5 | uSv/h: 0.0942827 | ✅ Excellent air quality. | ✅ Air is clean. | ✅ Background radiation—no risk..

    As I have built a few Geiger's in the past I ended up getting a calibrated Cesium-137 check disk. I use this to do a calibration of the sensor. (when I get this fully built I should rent a real Geiger Counter and compare the too) This Geiger is a solid state version on a tube and will detect pulsus right at first power on however it really is only good for gamma rays and it was mentioned it was best to sample over a 2 minute time before displaying results. You can still hold it up to a source/sample and if i add the piezo you should hear detections. It the sample is very high detections I would leave it alone/walk away, if it is a mild detection there is likely no harm to stay for the full two minute cycle to get a more scientific/actuate reading

    The counter is also calculated to give you a proper "uSv/h" (microsieverts per hour). Prob will need to redo the alpha calibration as it seems my last version was missing detections. However this code runs the radiation sensor at realtime. 

    alpha = 53.032  # uSv to CPM conversion factor
    cpm = pulse_count
    uSv_h = cpm / alph 

    GitHub link with the newest code - https://github.com/thedocdoc/AI-Field-Analyzer/tree/main   I also added a helpful little program that scan the I2C Bus

    I let it collect data for a bit then graphed out the data to get this: Based on the graph it might be good to do a bit of averaging on the TVOC and CO2 values, just not enough to skew the results. I.E take 3 readings in rapid / 3 = Result to print to OLED

  • Case redesign

    Apollo Timbers4 days ago 0 comments

    I printed the T1 case to hold in my hands and found it boxy and uncomfortable. So, I went back and redesigned the whole thing to actually feel good, thinner overall, with big fillets at the bottom for two-handed grip. It’s amazing what holding a prototype will teach you that CAD never will.

    Other upgrades:

    • Front hole for the radiation sensor. (I mean lets be honest we really want to be as far away as possible even if it's 4 inches) 

    • Small window under the display for the TSL2591 High Dynamic Range Digital Light Sensor. Placed so you don’t shadow it or block it with your fingers, and it’ll double as an ambient light sensor for auto-adjusting display contrast. \o/

    • Larger vent near the screen: Expanded by 3–4mm in each direction, with 1mm steel wire mesh on the inside for protection. Nothing says “field-ready” like mesh that can stop a grasshopper?

    • Active airflow: Added a 17x17mm, 3V fan to blow air in. This means sensors react faster, and in the worst-case scenario, it just might save your bacon. (this should allow for positive air flow and also cool off internal electronics, yea I'm looking at you Google Coral Dev Mini!) 

    • Tighter clamshell fit... You can actually pick it up and shake it now, no rattles, crazy right?

    Pro tip: If you do build this, don’t even think about bringing it through TSA. Especially not with Play-Doh inside. I’m not responsible for the “sir, please step over here” moment.

    Redesign after T1 design.

  • Software Tweaks

    Apollo Timbers5 days ago 0 comments

    I swapped the Adafruit KB2040 over for a Marble Pico I had around, it is a super charged Pico with a stemmaQT port 8mb of external flash onboard, and a handy SD card slot. Then became a big issue of aligning the right I2C ports. I jumped back and forth between micropython and cicuitpython as well and settled on circuitpython as it is more supported lib wise it seems... I have yet to get logging going but I did add a startup timer/countdown for the sensor and then a "static" display on the serial output. It works sort of, and really is a bad method of doing it, seems circuitpython does not really have the same functions for clearing the serial output. 

    I ended up with this "AI FIELD ANALYZER v1.0 | Time: 10:08:57 | CO₂: 408 ppm | TVOC: 0 ppb | ✅ Excellent air quality, no ventilation needed. | ✅ No significant VOCs detected—air is clean...ly."

    Unsure on the 64gig SD card may just be too much for it, plus I need a fat32 format as well. 

    Code is getting a bit long so, this is likely the last time I post it here and will be making a Github for it. 

    import time
    import board
    import busio
    import adafruit_sgp30
    
    # **Initialize I²C and Sensor**
    i2c = busio.I2C(board.GP5, board.GP4)
    sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
    sgp30.iaq_init()
    
    # **Function to Classify Risk Level**
    def get_warning(eCO2, TVOC):
        """Generates air quality warnings based on CO₂ & VOC readings."""
        warnings = []
        
        # **CO₂ Risk Levels**
        if eCO2 < 1000:
            warnings.append("✅ Excellent air quality, no ventilation needed.")
        elif 1000 <= eCO2 < 2000:
            warnings.append("⚠️ CO₂ levels rising—consider ventilating soon.")
        else:
            warnings.append("🚨 CO₂ dangerously high! Immediate ventilation needed!")
    
        # **VOC Risk Levels**
        if TVOC < 500:
            warnings.append("✅ No significant VOCs detected—air is clean.")
        elif 500 <= TVOC < 2000:
            warnings.append("⚠️ Chemical odors detected—monitor air closely.")
        else:
            warnings.append("🚨 High VOC levels! Ventilation or evacuation advised!")
    
        return warnings
    
    # **Startup Countdown (OLED-Friendly)**
    def sensor_startup_timer(seconds):
        """Displays countdown and clears it after startup."""
        for i in range(seconds, 0, -1):
            print("\r" + " " * 50, end="")  # Clears previous text
            print(f"\r⌛ Sensor Ready in {i}s...", end="")  # Overwrites same line
            time.sleep(1)
    
        print("\r✅ SGP30 Sensor Ready!", end="")  # Displays ready status for 2 seconds
        time.sleep(2)
    
        print("\033[2J\033[H", end="")  # **Fully clears screen before measurements start**
    
    
    # **Function to Update Display (OLED-Like Static Output)**
    def update_display(eCO2, TVOC):
        """Refreshes display while keeping output static."""
        current_time = time.localtime()
        formatted_time = f"{current_time[3]:02d}:{current_time[4]:02d}:{current_time[5]:02d}"
        warnings = get_warning(eCO2, TVOC)
    
        # **Clear previous output before writing new data**
        print("\r" + " " * 120, end="")  # Clears previous output fully
        print(f"\rAI FIELD ANALYZER v1.0 | Time: {formatted_time} | CO₂: {eCO2} ppm | TVOC: {TVOC} ppb | {warnings[0]} | {warnings[1]}", end="")
    
    
    # **Run Startup Timer**
    sensor_startup_timer(22)  # Adjust time as needed
    
    # **Sensor Loop**
    while True:
        eCO2, TVOC = sgp30.iaq_measure()
        update_display(eCO2, TVOC)
        time.sleep(1)

  • Humble beginnings

    Apollo Timbers5 days ago 0 comments

         So I was able to find one of the sensors I would like to have at the local Microcenter, The SGP30 Gas sensor. Took it home and found out it was a dud, after returning the new one started reporting right away. The following readings were a very "professional" test of me lighting a paper recipe near it lol...

    This is just a prototype/get it working situation, I plan on getting all of them hooked up to a Pi Pico and working together/reporting. I then can expand the code to the display. Once all of that is goo I can look at the Sensor PCB. The switch PCB however should be able to be done sooner then later as it is relatively simple. 

    I had a thought that once I do have them going it may be good to record sample sets in various areas, like a forest, my lab, the kitchen and so for so I build up a data set needed to help train the AI. 

    eCO2: 406 ppm, TVOC: 0 ppb

    ✅ Air quality is excellent. No ventilation needed.

    ✅ No significant VOCs detected. Air is clean.

    eCO2: 741 ppm, TVOC: 0 ppb

    ✅ Air quality is excellent. No ventilation needed.

    ✅ No significant VOCs detected. Air is clean.

    eCO2: 2595 ppm, TVOC: 397 ppb

    🚨 CO₂ is dangerously high! Leave within 5 min.

    ✅ No significant VOCs detected. Air is clean.

    eCO2: 5655 ppm, TVOC: 1810 ppb

    🚨 CO₂ is dangerously high! Leave within 5 min.

    ⚠️ Chemical odors detected—address within 15 min.

    eCO2: 2683 ppm, TVOC: 1219 ppb

    🚨 CO₂ is dangerously high! Leave within 5 min.

    ⚠️ Chemical odors detected—address within 15 min.

    eCO2: 847 ppm, TVOC: 586 ppb

    ✅ Air quality is excellent. No ventilation needed.

    ⚠️ Chemical odors detected—address within 15 min.

    Code...

    import time
    import board
    import busio
    import adafruit_sgp30
    
    # Initialize I2C and sensors
    i2c = busio.I2C(board.SCL, board.SDA)
    sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
    
    # Function to classify risk level
    def get_warning(eCO2, TVOC):
        warning = ""
    
        # CO₂ Alerts
        if eCO2 < 1000:
            warning += "✅ Air quality is excellent. No ventilation needed.\n"
        elif 1000 <= eCO2 < 2000:
            warning += "⚠️ CO₂ levels rising—consider ventilating within 10-15 min.\n"
        else:
            warning += "🚨 CO₂ is dangerously high! Leave within 5 min.\n"
    
        # VOC Alerts
        if TVOC < 500:
            warning += "✅ No significant VOCs detected. Air is clean.\n"
        elif 500 <= TVOC < 2000:
            warning += "⚠️ Chemical odors detected—address within 15 min.\n"
        else:
            warning += "🚨 High VOC levels! Leave within 5 min.\n"
    
        return warning
    
    print("SGP30 Sensor Ready")
    
    while True:
        eCO2, TVOC = sgp30.iaq_measure()
        print(f"eCO2: {eCO2} ppm, TVOC: {TVOC} ppb")
    
        # Generate warning message
        alert_message = get_warning(eCO2, TVOC)
        print(alert_message)
    
        time.sleep(1)
    

  • Flow charts are fun...

    Apollo Timbers6 days ago 0 comments

    Here is a proper flowchart on the internals of the device, still a work in progress but getting there I may swap out the magnetometer for something that has more axis and range. Decided to go with a speaker too as it will be able to give more then beeps, may still need to add a piezo beeper for low level warnings so I don't have to wake up the AI board. 

  • Pseudo Code and Risk thresholds

    Apollo Timbers6 days ago 0 comments

    Just dropped some pseudo-code based on my initial risk threshold chart in Python. The idea here is that this can run on a PI Pico 1 or 2, serving as the direct interface for all the sensors. It’s built to evaluate environmental risks, with preset thresholds that determine when to warn the user of danger.

    These warnings are recommendations, you can always choose to ignore them. However, if the AI spins up, a piezoelectric beeper will activate, and that means you should probably pay attention. At that point, the system has detected a high-risk event, like elevated CO₂ or radiation—stuff you don’t want to second-guess.

    Right now, this is still rudimentary and missing some functions I’ll need to fill in. But you have to start somewhere.

    I’ve also added the chart to the project's file base. Threshold values can be adjusted depending on how risk-averse you are.

    from machine import I2C, Pin, UART
    from ssd1306 import SSD1306_I2C
    from time import sleep, ticks_ms
    
    # --- Init Display & UART ---
    i2c = I2C(0, scl=Pin(1), sda=Pin(0))
    oled = SSD1306_I2C(128, 64, i2c)
    uart = UART(0, baudrate=115200, tx=Pin(4), rx=Pin(5))
    
    non_critical_interval = 60_000  # 1 minute
    last_non_critical_check = 0
    
    def warn_user(sensor, message):
        oled.fill(0)
        oled.text("⚠ {}".format(sensor), 0, 0)
        oled.text(message, 0, 10)
        oled.show()
    
    def wake_ai(snapshot):
        uart.write("SNAPSHOT:" + snapshot + "\n")
    
    def handle_risk(level, sensor, med_msg, high_msg, snapshot):
        if level == "LOW":
            return
        elif level == "MEDIUM":
            warn_user(sensor, med_msg)
        elif level == "HIGH":
            warn_user(sensor, high_msg)
            wake_ai(snapshot)
    
    def eval_risk(value, thresholds):
        low, med = thresholds
        if value < low: return "LOW"
        elif value <= med: return "MEDIUM"
        return "HIGH"
    
    # --- AI Message Handler ---
    def process_ai():
        if uart.any():
            msg = uart.readline().decode().strip()
            oled.fill(0)
            if msg.startswith("CRITICAL:"):
                oled.text("AI:", 0, 0)
                oled.text(msg[9:], 0, 10)
            elif msg.startswith("INFO:"):
                oled.text("Info:", 0, 0)
                oled.text(msg[5:], 0, 10)
            oled.show()
    
    # --- Sensor Read Functions (stubbed) ---
    def read_all_critical():
        return {
            "CO2": 1600.0,
            "VOC": 1.2,
            "Pressure": 985.0,
            "Radiation": 0.7
        }
    
    def read_all_non_critical():
        return {
            "Temp": 34.0,
            "Humidity": 72.0,
            "Light": 3100,
            "Sound": 89,
            "Mag": 350
        }
    
    # --- Loop ---
    while True:
        now = ticks_ms()
        critical = read_all_critical()
        snapshot = ",".join(f"{k}={v}" for k, v in critical.items())
    
        handle_risk(eval_risk(critical["CO2"], (1000, 2000)), "CO2", "Ventilate", "High CO₂!", snapshot)
        handle_risk(eval_risk(critical["VOC"], (0.5, 2.0)), "VOC", "Irritants", "Toxic VOCs", snapshot)
        handle_risk(eval_risk(critical["Pressure"], (980, 1000)), "Pressure", "Storm risk", "Severe drop", snapshot)
        handle_risk(eval_risk(critical["Radiation"], (0.5, 5.0)), "Radiation", "Elevated", "Evacuate!", snapshot)
    
        if now - last_non_critical_check > non_critical_interval:
            noncrit = read_all_non_critical()
            snapshot_nc = ",".join(f"{k}={v}" for k, v in noncrit.items())
            handle_risk(eval_risk(noncrit["Temp"], (15, 30)), "Temp", "Discomfort", "Heat/Hypo!", snapshot_nc)
            handle_risk(eval_risk(noncrit["Humidity"], (30, 60)), "Humidity", "Uncomfortable", "Resp. risk", snapshot_nc)
            handle_risk(eval_risk(noncrit["Light"], (500, 2000)), "Light", "Glare", "Overexposed", snapshot_nc)
            handle_risk(eval_risk(noncrit["Sound"], (70, 85)), "Sound", "Noisy", "Hearing danger", snapshot_nc)
            handle_risk(eval_risk(noncrit["Mag"], (100, 300)), "Mag Field", "Elevated", "Interference", snapshot_nc)
            last_non_critical_check = now
    
        process_ai()
        sleep(5)  # Sleep time between wake cycles
    

  • Power Management Strategy - The Three-Tier Approach

    Apollo Timbers7 days ago 0 comments

    I've been chewing on the power problem for this thing. (It's going to be a hog). However, the goal of having AI onboard isn't to show off, it's to say, "Hey, something's not right you should take a look and a warning of immediate danger to the human/group," without you needing to Google whether 1200ppm CO₂ is bad (spoiler: it is after a bit).

    But here's the catch: if the AI runs 24/7, the battery's toast in an hour or two.

    So here's my solution:

    A Three-Tier Power System...

    Tier 1 – Basic Threshold Monitoring
    Always on. No AI, just dumb math: “Is radiation high?” “Is CO₂ dangerous?” If yes, flag it. This barely sips power and helps you know when to GTFO.
    Battery: 24+ hours.

    Tier 2 – “Something’s Weird” Detection
    This is the AI’s cue. When multiple sensors go funky (say, pressure drops and VOCs spike), the Coral spins up, analyzes the pattern, and tells you what’s likely going on in plain English. Then it shuts back down. More power draw here, but it only runs as needed.
    Battery: 8–12 hours, depending on how much weirdness you run into.

    Tier 3 – Full Analysis Mode
    Everything wakes up: full AI, screen, every sensor, the whole nine yards. For serious events or user-triggered deep dives. This burns battery quick, but that’s expected.
    Battery: 1–2 hours max.

    Smarter Display Use: (this is huge)
    Screen stays off unless:

    • Something’s actually wrong

    • You press a button

    • The AI has something important to say

    Otherwise, a simple red heartbeat LED handles status.

    Sensor Prioritization:
    Not every sensor needs to run constantly:

    • Critical: Radiation, CO₂ – always monitored

    • Environmental: Temp, humidity – checked periodically (can power off with MOSFET)

    • Secondary: Sound, magnetism – only wake up when needed

    Power Targets (aiming for these):

    • Normal field use: 24+ hours

    • Monitoring mode: 8-12 hours (screen off, AI rarely active)

    • Emergency mode: 1–2 hours (everything running full blast)

    Main idea: Most of the time, nothing interesting is happening. The device should act like it—no one wants a gadget that dies early or screams about nothing.

    Next up: finish the sensor PCB layout, Switch PCB, and figure out the Coral’s power switching. Still not a great programmer, but the hardware’s shaping up.

    Next up is finishing the sensor PCB layout, Switch PCB and figuring out the Coral's power switching. I'm still no great programmer, but the hardware's shaping up.

  • Getting the case right

    Apollo Timbers7 days ago 0 comments

    I’ve found that if you have access to CAD, it’s always better to design as much of the final project in virtual first.

    This has a lot of advantages:

    • Stuff just fits on the first try if you model it accurately

    • You can save money by knowing the exact components going into the build

    • You get to preview what it’ll look like, and make changes before committing

    I’m printing the initial case (top and bottom) without the details like bosses and mounting points, just to get it in my hands and see if it’s comfortable to hold and easy to store.

    Some of the initial design details:

    • Clamshell case for the top and bottom

    • Mounting bosses for assembly

    • Double-checked that the OLED screen’s PCB will fit in the space provided

    I still need to add an external charging port (likely USB-C). I’ll see if this can handle data too—if not, I might add an SD card slot as an easy way to store the collected data.

    Next, I’ll see if there’s enough room for the main components: the dev board, battery, Pocket Geiger, and other sensors. There will likely be at least two custom PCBs—one for the switches, and one for the bulk of the sensors. If everything fits well, I probably won’t try to make it smaller; I want it to feel substantial in the hand.