{"slug": "petlibro-polar-reverse-engineering-notes", "title": "Petlibro Polar Reverse Engineering Notes", "summary": "A developer reverse-engineered the Petlibro Polar (PLAF109/AF109) pet feeder firmware, mapping the ESP32-C3's GPIO and AW9523B I/O expander pins to their functions. The analysis, performed offline with GPT 5.6 Sol, provides a pin map for future replacement firmware, highlighting a GPIO9 conflict with the ROM download strap.", "body_md": "note: these findings were done with GPT 5.6 Sol.\n\nThis document records static analysis of the stock PLAF109/AF109 application firmware on disk. It is intended as a starting point for a future replacement firmware, not as permission to energize actuators from an incomplete pin map. The analysis was offline; no device or Petlibro service was contacted.\n\nFile: `firmware/PLAF109_OTA_020032.bin`\n\n- File SHA-256:\n`9e73f904e7f6a09172b7384aa84fa38963ce75ef22bebec89f38ebef5c8dd53d`\n\n- Target: ESP32-C3, revision 0.3 or newer\n- Application:\n`AF109`\n\nversion`2.0.32`\n\n- Build: 2025-04-22 09:46:39, ESP-IDF\n`v4.4.6-dirty`\n\n- Image size: 1,247,744 bytes\n- Entry point:\n`0x40380450`\n\n- IROM:\n`0x42000020`\n\nthrough`0x420ec8cf`\n\n- DROM:\n`0x3c0f0020`\n\nthrough`0x3c11bcc7`\n\nThis is only an OTA application image. Values kept in NVS, including the unit's motor-current calibration and thresholds, are not in this file.\n\nThe ESP32-C3 directly handles ADC inputs, interrupts, one PWM output, and a\none-wire sound interface. Most actuator outputs are provided by an AW9523B\n16-bit I/O expander at I2C address `0x58`\n\n.\n\nThe central board initializer at `0x4200d726`\n\ncalls these routines in order:\n\n- AW9523B and I2C initialization at\n`0x4200d94a`\n\n- cooler/PTC output initialization at\n`0x4200d1c0`\n\n- plate motor initialization at\n`0x4200d134`\n\n- door motor initialization at\n`0x4200d17a`\n\n- button initialization at\n`0x4200d09e`\n\n- alarm/status LED initialization at\n`0x4200d0f4`\n\n- sensor and 38 kHz LEDC initialization at\n`0x4200cdb6`\n\n- ADC initialization at\n`0x4200d200`\n\n- sound interface initialization at\n`0x4200d6ca`\n\nThe AW9523 shadow-register bit writer is at `0x4200d838`\n\n. It updates a 16-bit\nRAM shadow and writes AW9523 output registers `0x02`\n\nand `0x03`\n\nthrough the I2C\nhelper at `0x4200d816`\n\n.\n\nConfidence in this table is high: the assignments come from immediate values passed to ESP-IDF GPIO, ADC, I2C, and LEDC functions, then were correlated with the firmware's retained log labels.\n\n| ESP GPIO | Direction/peripheral | Recovered purpose | Evidence |\n|---|---|---|---|\n| 0 | digital input, any-edge | DC/battery power-source detection | polled by `Power_Handle` at `0x4200c692` |\n| 1 | ADC1 channel 1 | NTC temperature divider | read by `Read_Ntc_Temp` at `0x4200d41a` |\n| 2 | LEDC output | 38 kHz optical/IR emitter, 8-bit timer, duty 127 or 0 | LEDC setup under `0x4200cdb6` |\n| 3 | ADC1 channel 3 | door motor current | retained assertion names `ADC1_CHAN3_MOTOR` |\n| 4 | ADC1 channel 4 | battery voltage | firmware converts calibrated mV and applies a 2:1 divider factor |\n| 5 | ADC2 channel 0 | plate motor current | retained assertion names `ADC2_CHAN0_MOTOR` |\n| 6 | digital output | one-wire sound/voice IC data | byte sender at `0x4200d77a` |\n| 7 | I2C0 SDA | AW9523B data | pull-up enabled |\n| 8 | I2C0 SCL | AW9523B clock | pull-up enabled, 100 kHz bus |\n| 9 | digital output | AW9523B reset | held low for 200 ms, then high |\n| 10 | interrupt input, pull-up, any-edge | rotating tray microswitch/index pulse | `Plate_Micro_Handle` at `0x4200e332` |\n| 18 | interrupt input, any-edge | user/reset button | `Key_init` and `Timer_KeyRestoreHandle` |\n| 19 | interrupt input, any-edge | door/close-position signal | retained label `Close_SW_Interrupt` |\n| 20 | interrupt input, any-edge | IR/pet-presence receiver | `Infrared_Handle` at `0x4200c596` |\n\nGPIO9 is an important board-specific conflict: it resets the AW9523B in the running application, but it is also the ESP32-C3 ROM download-mode strap. A replacement firmware must release it appropriately after boot.\n\nThe firmware treats AW9523 pins as one flat bit index from 0 through 15.\n\n| Bit | AW9523 pin | Recovered purpose | Initial state |\n|---|---|---|---|\n| 0, 1 | P0.0, P0.1 | hardware revision straps/inputs | read during startup |\n| 4, 7 | P0.4, P0.7 | paired existence/optical sensing controls | application controlled |\n| 5, 6 | P0.5, P0.6 | rotating plate motor H-bridge inputs | high/high |\n| 8, 9 | P1.0, P1.1 | two-channel front/alarm status LED | low/low |\n| 12 | P1.4 | cooler/PTC power control | high |\n| 14, 15 | P1.6, P1.7 | feeding-door motor H-bridge inputs | high/high |\n\nThe startup strap decode masks the low two AW9523 input bits. Static control\nflow maps value `2`\n\nto hardware `5.1.0`\n\n, value `1`\n\nto `5.2.0`\n\n, and other values\nto `5.0.0`\n\n. This agrees with the observed unit reporting hardware `5.1.0`\n\n, but\nthe electrical pull-up/pull-down arrangement is not present in the OTA image.\n\nThe plate motor is a bidirectional DC motor behind two H-bridge inputs on AW bits 5 and 6. The stock firmware uses:\n\n- high/high as\n`GPIO_MOTOR_BRAKE`\n\n- high/low and low/high for the two directions\n- motor-current feedback on ESP GPIO5 / ADC2 channel 0\n- a tray index microswitch on ESP GPIO10\n\nThe direction combinations should initially be called direction A and direction B. Static analysis does not establish which is clockwise at the physical tray without a board observation.\n\n`Plate_Micro_Handle`\n\nat `0x4200e332`\n\nadvances the current position on each\naccepted switch event, wrapping through positions 1, 2, and 3. When the current\nposition equals the requested position, the routine writes high/high to AW\nbits 5 and 6 and changes the plate motor state to the stopped/braked state.\nThe retained logs include `F1`\n\n, `F2`\n\n, `PLATE_READY_SET_POS`\n\n,\n`PLATE_READY_BACK_POS`\n\n, and `SET_PLATE_POS_SERVICE`\n\n, showing that normal\npositioning and return/reposition behavior are separate state-machine paths.\n\nThe plate controller does not rely on time alone. It watches motor current and\nuses retained attributes named `plateStuckCurrent`\n\nand `plateStuckTimeout`\n\n.\nOn a detected stall it enters `MOTOR_STUCK`\n\n, brakes the motor, and inhibits\nnormal cooling/feeding transitions. The actual current and timeout values are\nloaded from persistent device attributes and cannot be recovered from this OTA\napplication alone.\n\nThe door motor is another bidirectional DC motor behind AW bits 14 and 15.\nLike the plate motor, high/high is its initialized stopped/braked state and the\ntwo asymmetric combinations select opposite directions. Firmware logs name\nthe directions `GPIO_DOOR_MOTOR_FORWARD`\n\nand `GPIO_DOOR_MOTOR_BACK`\n\n; higher\nlevel paths associate them with closing and opening respectively. Verify that\nphysical mapping before using it in replacement firmware because the motor or\nwiring orientation may vary by board revision.\n\nDoor movement uses both:\n\n- a position/close signal on ESP GPIO19\n- motor-current feedback on ESP GPIO3 / ADC1 channel 3\n\nThe state machine has `DOOR_READY_OPEN`\n\n, `DOOR_MOTOR_OPEN_OVER`\n\n,\n`DOOR_READY_CLOSE`\n\n, `DOOR_MOTOR_CLOSING`\n\n, `DOOR_MOTOR_CLOSE_OVER`\n\n, and\n`MOTOR_STUCK`\n\nstates. A door-reset/recovery path samples at approximately\n100 ms intervals with a roughly 50-sample bound, about five seconds total.\nRetained attributes `doorStuckCurrent`\n\n, `doorStuckTimeout`\n\n,\n`doorCheckSignalTime`\n\n, and `doorNotcheckSignalTime`\n\nshow that both current and\nthe position signal participate in protection. Their per-unit values are in\nNVS, not this image.\n\nThe feeding sequence is explicitly staged as plate movement/thaw, door open, feeding duration, and door close. It is not safe to expose independent raw motor buttons without preserving those interlocks and recovery states.\n\nThe only identified refrigeration output is AW bit 12, called `PTC`\n\nby the\nfirmware. No separate fan output was identified. The temperature input is an\nNTC divider on ESP GPIO1 / ADC1 channel 1.\n\n`Read_Ntc_Temp`\n\nat `0x4200d41a`\n\naverages ten ADC readings and applies a standard\n10 kOhm, B3950 thermistor calculation using 3.3 V, `1 / 298.15`\n\n, and\n`-273.15`\n\n. A retained `tempCoeff`\n\npermits unit calibration. Temperature\npublication is held off during a 60-cycle stabilization period.\n\nThe cooler controller at `0x4200e672`\n\ncontains exact double constants 12.0 and\n13.0 degrees C at DROM addresses `0x3c0fae38`\n\nand `0x3c0fae30`\n\n, providing a\n1 degree hysteresis band. It also has a 600-call dwell counter and guards for\npower source, feeding/plate activity, motor-stuck state, and explicit cool\nswitch state. Logs include `start cool`\n\n, `NOT_COOL`\n\n, `DL_COOL_STOP_STATUS`\n\n,\n`switch plate make cool`\n\n, and `GPIO_PTC_ON`\n\n.\n\nDo not assume the electrical polarity of AW bit 12 yet. Initialization sets it\nhigh, while runtime paths explicitly write both high and low around the\ntemperature comparisons and guard states. The retained `GPIO_PTC_ON`\n\nlog is\nnot enough by itself to prove whether the expander output or a downstream\ntransistor is active-low. Confirm polarity with an unpowered continuity check\nand then a current-limited measurement before enabling refrigeration.\n\nThe ESP32-C3 does not synthesize audio with I2S, DAC, or PWM. GPIO6 drives an\nexternal one-wire sound or voice IC using the byte sender at `0x4200d77a`\n\n.\nThe recovered waveform is:\n\n- idle high\n- 5 ms low start pulse\n- eight data bits, least-significant bit first\n- bit 0: 1 ms high, then 3 ms low\n- bit 1: 3 ms high, then 1 ms low\n- return high after the eighth bit\n\nThe MQTT/application `AUDIO`\n\ncommand path at `0x42018988`\n\nsends byte `0x01`\n\n,\nwhich is therefore the stock one-shot feeding bell command. Other internal\npaths also send `0xef`\n\n; its sound or control meaning is unresolved. Persistent\nfields include `enableAudio`\n\nand `audioTimes`\n\n, so scheduled feeding sounds can\nbe suppressed or repeated independently of the one-shot command.\n\nThe external sound IC model and its complete command table cannot be derived\nfrom this application image. Identify the package marking or capture GPIO6\nwhile exercising stock firmware before assuming commands other than `0x01`\n\n.\n\nThe front indicator uses AW bits 8 and 9. `AlarmLed_init`\n\nat `0x4200d0f4`\n\ninitializes both low. A `dl_led`\n\ntask drives combinations of the two outputs to\nrepresent states and honors retained settings including `lightSwitch`\n\n,\n`enableLight`\n\n, `led_enable`\n\n, `led_mode`\n\n, and start/end hour and minute fields.\n\nThe OTA image establishes that this is a two-channel indicator, but not which bit maps to which physical color or whether either channel is active-low. Observe the AW outputs alongside visible stock patterns before naming colors in custom firmware.\n\n- GPIO0 distinguishes external DC and battery operation. Stock logic disables or restricts cooling and networking behavior in battery mode.\n- GPIO4 measures battery voltage through a divider; firmware doubles the calibrated millivolt result before estimating charge state.\n- GPIO2 generates a 38 kHz, 50-percent-duty carrier and GPIO20 handles an infrared/presence event. This strongly indicates a modulated optical sensing pair used for pet/existence detection.\n- AW bits 4 and 7 are toggled together in related existence-detection paths. Their exact external circuits are not identifiable from the OTA image.\n- GPIO18 is the user button. The stock code uses debounce/restore timers and includes network/reset behavior, so a custom build should distinguish short and long presses before assigning destructive actions.\n\nBefore driving the motors or cooler, a replacement should preserve at least:\n\n- AW9523 reset, I2C shadowing, and stopped output states before task startup.\n- high/high motor brake behavior verified against the actual H-bridge.\n- tray microswitch debounce and modulo-3 position tracking.\n- an explicit homing/reposition procedure for unknown startup position.\n- current-based plate and door stall cutoffs, plus hard movement timeouts.\n- door position-signal validation while closing.\n- motor inhibition after a stall until an attended recovery action.\n- cooler hysteresis, minimum dwell time, battery-mode inhibition, and inhibition during motor movement or feeding transitions.\n- conservative startup defaults for both LED channels and the sound line.\n- retained calibration storage with bounds checking and recoverable defaults.\n\nDo not copy unknown stock NVS values blindly between units. First obtain the\nfull flash backup described in `CUSTOM_FIRMWARE.md`\n\n, identify the relevant NVS\nkeys, and correlate them with current-limited bench measurements.\n\nInspect the image header and segment map:\n\n```\npy -3.10 -m esptool image-info firmware\\PLAF109_OTA_020032.bin\n```\n\nResolve direct references to retained board-support labels:\n\n```\npy -3.10 analysis\\esp32c3-xrefs.py `\n  firmware\\PLAF109_OTA_020032.bin `\n  Motor_init Door_Motor_init Ptc_init AlarmLed_init Adc_init `\n  Read_Ntc_Temp BSP_aduioInit AW9523B_i2cInit\n```\n\nHigh-value virtual addresses:\n\n| Address | Meaning |\n|---|---|\n`0x4200c596` |\ninfrared input handler |\n`0x4200c692` |\npower-source input handler |\n`0x4200cdb6` |\nGPIO2 LEDC and sensor setup |\n`0x4200d0f4` |\ntwo-channel status LED initialization |\n`0x4200d134` |\nplate motor initialization |\n`0x4200d17a` |\ndoor motor initialization |\n`0x4200d1c0` |\nPTC/cooler output initialization |\n`0x4200d200` |\nADC setup |\n`0x4200d41a` |\nNTC conversion |\n`0x4200d6ca` |\nsound GPIO initialization |\n`0x4200d77a` |\none-wire sound byte sender |\n`0x4200d816` |\nAW9523 register write helper |\n`0x4200d838` |\nAW9523 bit shadow/write helper |\n`0x4200d94a` |\nI2C and AW9523 initialization |\n`0x4200e332` |\ntray microswitch and position handler |\n`0x4200e672` |\ncooler controller |\n`0x42018988` |\none-shot `AUDIO` command dispatch |\n\nUseful retained strings include `GPIO_MOTOR_BRAKE`\n\n, `plateStuckCurrent`\n\n,\n`doorStuckCurrent`\n\n, `Close_SW_Interrupt`\n\n, `MicroSw_DebounceHandle`\n\n,\n`GPIO_PTC_ON`\n\n, `Infrared_Handle`\n\n, `AUDIO`\n\n, `lightSwitch`\n\n, and `audioTimes`\n\n.\n\n- cooler output polarity and downstream power-driver topology\n- AW bits 8/9 physical LED colors and polarity\n- plate direction A versus physical clockwise/counterclockwise movement\n- exact door switch active level and motor orientation on each board revision\n- per-unit plate and door stall-current thresholds from NVS\n- external sound IC model and commands other than confirmed bell byte\n`0x01`\n\n- exact circuits controlled by AW bits 4 and 7\n- absolute tray position after startup when no trusted retained position exists\n\nResolve these with the full flash backup, board photographs and part markings, continuity checks while unpowered, and current-limited logic measurements of stock firmware. Do not begin by connecting normal actuator power to an untested replacement build.", "url": "https://wpnews.pro/news/petlibro-polar-reverse-engineering-notes", "canonical_source": "https://gist.github.com/JakeShirley/919704420b309d5040c7e55aff5a5601", "published_at": "2026-08-24 05:29:20+00:00", "updated_at": "2026-08-26 17:16:26.778267+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Petlibro", "ESP32-C3", "AW9523B", "GPT 5.6 Sol", "PLAF109", "AF109"], "alternates": {"html": "https://wpnews.pro/news/petlibro-polar-reverse-engineering-notes", "markdown": "https://wpnews.pro/news/petlibro-polar-reverse-engineering-notes.md", "text": "https://wpnews.pro/news/petlibro-polar-reverse-engineering-notes.txt", "jsonld": "https://wpnews.pro/news/petlibro-polar-reverse-engineering-notes.jsonld"}}