The STM32 uses (at least) the following libraries:
- QP real-time embedded frameworks, version
6.7.0
, with theQV
kernel. It shows build date 03/11/22 and time 09:12:08. The QP software has a number of configurable options which are configured as follows:Q_SIGNAL_SIZE
: 2QF_EVENT_SIZ_SIZE
: 2QF_TIMEEVT_CTR_SIZE
: 4QF_EQUEUE_CTR_SIZE
: 1QF_MPOOL_SIZ_SIZE
: 2QF_MPOOL_CTR_SIZE
: 2QS_OBJ_PTR_SIZE
: 4QS_FUN_PTR_SIZE
: 4QS_TIME_SIZE
: 4QF_MAX_ACTIVE
: 32QF_MAX_TICK_RATE
: 2QF_MAX_EPOOL
: 3
- A modified newlib as C library.
Various message-handling modules are created:
RS485Arbiter
. Controls the RS485 bus used to communicate with the LEDs.DoorManager
. Deals with the door open/close sensor.TempHumManager
.LEDManager
. Communicates with the LEDs.FanManager
CoolingManager
PumpManager
TankManager
. Deals with the tank water level sensor. (There's a magnet in the water tank's float and hall effect sensors in the Plantcube's wall near where it floats.)ValveManager
ECManager
. Unclear what this is for. It reads an ADC value.TempController
WaterController
NetworkControllerSend
. Sends messages to the ESP8266.NetworkControllerReceive
. Receives messages from the ESP8266.LightController
RecipeService
. Deals with recipe files (see below).
These were the reason for reverse-engineering the STM32 code in the first place, as they were the only non-obvious MQTT message to the ESP8266. Many Bothans died to bring us this information. Seriously, it took many hours of reverse engineering to work out the format and content of the recipe file, because it touches many parts of the STM32 code and its format is... not conducive to easy reverse engineering.
The file starts with a header:
Bytes | Content |
---|---|
00-03 | Timestamp / Recipe ID. Little-endian Unix timestamp. |
04-07 | cycle_start_time . Little-endian Unix timestamp. |
08 | Layer count minus 1. In all recipes seen, this was 2. |
09 | Recipe version. Must be 0x07. |
0a... | num_blocks for each layer in byte 08. Normally 0a-0c. |
num_blocks
in recipes seen has been 02
for one of the layers, 01
for
another and 00
for the third. Which of the first two was 02
and which was
01
varied.
The header is followed by sum(num_blocks)
two-byte blocks (so, 3 of them for
the recipes seen):
Bytes | Content |
---|---|
00 | num_periods . The number of periods in this block. |
01 | repetition_limit . See "Finding the period" below. |
This is followed by a series of period
s, each of which is 14 bytes long:
Bytes | Content |
---|---|
00-03 | period_length in seconds. Little-endian 32-bit integer. |
04-07 | Light channel values (different colours). Four * one byte, 0-100. |
08-09 | Temperature target. Little-endian 16-bit integer, degrees C * 100. |
0a-0b | Water target. Little-endian 16-bit integer. |
0c-0d | Watering time offset in seconds. Little-endian 16-bit integer. |
This code is odd. To find the currently active period, the algorithm seems to be:
- Subtract the header's
cycle_start_time
from the current Unix time plus thetotal_offset
(see the MQTT docs). The cycle start time can be up to 6 months ago, so this potentially leaves a remainder of several million seconds. Let's call thatremainder
. repetition_count = 0
. Also, set current period to be the first period of the first block.- If
remainder
is less than 0, finish, we found the period. (This very likely won't be true on the first iteration.) remainder = remainder - period_length
, for the current period.- Move to the next period of the current block. If this was the last period of
the current block,
repetition_count = repetition_count + 1
. - If
repetition_count == repetition_limit
for the current block, make the current period be the first period of the next block. If this was the last block, loop back to the first block.
The recipes seen have one block with a single period of 86400s (one day) and a
repetition_limit
that varies over time, as the distance from
cycle_start_time
increases (i.e. the recipe received from AWS was unchanged
other than an increased repetition_limit
). The recipes then had a second
block with two periods, one of 30600s (8.5h) and one of 55800s (15.5h). The
algorithm above would therefore result in subtracting a bunch of whole days
(the repetition_limit
of the first block) before attempting to find the actual
current period by looping through the second block.
For seen recipes, for the sleep period:
- Light channel values are set to
0x00
for all four channels. - Temperature target is set to 20.00C.
- Water target is set to 0.
- Watering time is set to 28800s, or -1s.
For seen recipes, for the daytime period:
- Light channel values are set to
0x3d
,0x27
,0x21
and0x0a
. - Temperature target is set to 23.00C.
- Water target is set to 70.
- Watering time is set to 28800s or 28803s.
The ESP8266 and STM32 pass messages back and forth to one another. The following are all permitted messages from the ESP to the STM:
0x02 Init, respond with CONNECTED
0x04 Ack
0x08 Recipe
0x09 Ignored? No code found that processes this message.
0x0A Set Unix time
0x0B Something with subcommands to change mode, pause drain tank, continue drain tank. Cleaning?
0x0C MCU update (firmware?)
0x0E Recipe adjust
0x10 NCU update (doesn't look like this gets handled)
0x12 Restart
0x13 Set debug level
0x14 Factory reset
0x17 Cancel water down time?
0x18 Manual watering layer A
0x19 Manual watering layer B
0x1A Network connect?
QP uses signals for communication between modules. The signals listed below are used. Those not listed don't appear to be used. Those listed with "???" are used but their name/function hasn't yet been discovered.
0x01 Q_ENTRY_SIG
0x02 Q_EXIT_SIG
0x03 Q_INIT_SIG
0x04 Q_USER_SIG
0x05 MODE_CHANGE_SIG
0x06 VERBOSE_REPORTING_SIG
0x07 GRACEFUL_SHUTDOWN_SIG
0x08 DOOR_OPEN_SIG
0x09 DOOR_CLOSE_SIG
0x0a NETWORK_CONNECT_SIG
0x0c MANAGER_ERROR_SIG
0x0d ???
0x0f MANAGER_SHUTDOWN_SIG
0x10 MANAGER_REFRESH_SIG
0x11 I2C_TX_DONE_SIG
0x12 I2C_RX_DONE_SIG
0x13 USART_RECEIVED_BYTE_SIG
0x14 USART_WRITE_BYTE_SIG
0x15 ONOFF_SET_SIG
0x16 ONOFF_CHANGE_SIG
0x17 DEVICE_ON_SIG
0x18 DEVICE_OFF_SIG
0x19 GPIO_IT_CHANGE_SIG
0x1a SAMPLE_PIN_SIG
0x1b CONTROLLER_MODE_SIG
0x1c DEBUG_MODE_ON_SIG
0x1d DEBUG_MODE_OFF_SIG
0x1e SERVICE_ERROR_SIG
0x1f LAYER_A_SIG
0x20 LAYER_B_SIG
0x21 APPLIANCE_SIG
0x22 SOFTWARE_ERROR_SIG
0x23 DOOR_API_SIG
0x24 DEBUG_MESSAGE_SIG
0x25 TEMP_CHANGE_SIG
0x26 HUM_CHANGE_SIG
0x27 TEMPHUM_TIMEOUT_SIG
0x28 FAN_SET_SIG
0x29 FAN_BOOST_SIG
0x2a FAN_TACHO_SIG
0x2b FAN_RAW_RESULT_SIG
0x2c FAN_GET_SPEED_SIG
0x2d FAN_TACHO_ERROR_SIG
0x2e FAN_STOP_SIG
0x2f FAN_START_SIG
0x30 FAN_RAMP_SPEED_SIG
0x31 TANKLVL_CHANGE_SIG
0x32 TANKLVL_RAW_CHANGE_SIG
0x33 TANKLVL_AVAILABLE_SIG
0x34 EC_CHANGE_SIG
0x35 EC_SETUP_DONE_SIG
0x36 EC_TIMEOUT_SIG
0x37 EC_RAW_RESULT_SIG
0x38 EC_REPORT_SIG
0x39 RFID_CHANGE_SIG
0x3a VALVE_SET_SIG
0x3b VALVE_OPENED_SIG
0x3c VALVE_CLOSED_SIG
0x3e VALVE_CHANGE_SIG
0x3f LED_SET_SIG
0x40 LED_TRANSFER_READY_SIG
0x41 LED_BUS_ERROR_SIG
0x42 LED_BOOT_SIG
0x43 LED_REQUEST_BUS_SIG
0x44 LED_TRANSFER_DONE_SIG
0x45 LED_BAUDRATE_SIG
0x46 LED_ADDRESS_SIG
0x47 LED_EXT_RESET_SIG
0x48 LED_RESET_CONFIG_SIG
0x49 LED_POLL_SIG
0x4a LED_BOARD_STATUS_SIG
0x4b RS485_BUS_GRANTED_SIG
0x4c RS485_LED_SWITCH_SIG
0x4d WATER_TARGET_SIG
0x4e DRAIN_TANK_MODE_ON_SIG
0x4f DRAIN_TANK_MODE_OFF_SIG
0x50 DRAIN_TANK_TIMEOUT_SIG
0x51 CANCEL_WATER_DOWN_TIME_SIG
0x52 MANUAL_WATERING_SIG
0x53 PAUSE_DRAIN_TANK_SIG
0x54 CONTINUE_DRAIN_TANK_SIG
0x55 SILENT_MODE_WATERING_SIG
0x56 ABORT_WATER_SIG
0x57 WATER_CYCLE_SIG
0x58 CHECK_QUEUE_SIG
0x59 SENSOR_GAP_TIMEOUT_SIG
0x5a TANK_EMPTY_SIG
0x5b TANK_REFILL_SIG
0x5c
0x5e AFTER_WATERING_2H_SIG
0x5f AFTER_WATERING_3H_SIG
0x60 TEMP_TARGET_SIG
0x61 COMPRESSOR_ECO_SIG
0x62 COMPRESSOR_UNLOCK_SIG
0x63 COMPRESSOR_MAX_SIG
0x64 MAX_RECIPE_TEMP_SIG
0x65 REPORT_ECO_WARNING_SIG
0x66 UNKNOWN_66_SIG
0x67 LIGHT_TARGET_SIG
0x68 DOOR_UPDATE_LED_SIG
0x69 AFTERCOOLING_DONE_SIG
0x6a NETCTRL_MCU_ACK_SIG
0x6b NETCTRL_NCU_ACK_SIG
0x6c NETCTRL_TIMEOUT_SIG
0x6d NETCTRL_FEED_IWDG_SIG
0x6e NETCTRL_CONNECTED_SIG
0x6f NETCTRL_NCU_UPDATE_SIG
0x70 NETCTRL_MCU_UPDATE_SIG
0x71 NETCTRL_YMODEM_RECEIVE_SIG
0x72 NETCTRL_YMODEM_ERROR_SIG
0x74 NETCTRL_YMODEM_ACK_SIG
0x75 NETCTRL_YMODEM_NAK_SIG
0x76 NETCTRL_YMODEM_C_SIG
0x77 NETCTRL_YMODEM_ACK_C_SIG
0x78 NETCTRL_YMODEM_END_SIG
0x79 NETCTRL_YMODEM_START_SIG
0x7a NETCTRL_YMODEM_SENDER_ABORT_SIG
0x7b NETCTRL_YMODEM_RECEIVER_ABORT_SIG
0x7c NETCTRL_YMODEM_SUCCESS_SIG
0x7d NETCTRL_YMODEM_READY_SIG
0x7e NETCTRL_YMODEM_KILL_SESSION_SIG
0x7f RECIPE_CHANGE_SIG
0x80 RECIPE_START_SIG
0x81 RECIPE_ADJUST_SIG
0x82 RECIPE_CONFIRMATION_SIG
0x83 RECIPE_NEW_PERIOD_SIG
0x84 SUN_SET_RISE_START?
0x85 SUN_SET_RISE_FINISHED?
0x86 SUN_SET_RISE_SIG
0x87 SILENT_MODE_END_SIG
The RTC module of the STM32, in addition to storing the date and time, offers 20 bytes of battery-backed storage for user data. The Plantcube uses these bytes as follows:
0x00 Set to 0xdecea5ed - this is used as a flag value for validity of the RTC data.
0x01 Recipe time offset (must be <86400).
0x02 Assertion timestamp.
0x03 Assertion line number.
0x04-0A Assertion module.
0x0B ?
0x0C ?
0x0D ?
0x0E ?
0x0F ?
0x10 ?
0x11 ?
0x12 Debug level
0x13 ?
The assertion data is stored by the Plantcube's Q_onAssert
function when one
of the code's assertions fails.