Skip to content

after finish write file via ftp service device prefom RunReason.AUTO_RELOAD ( on battery only ) #10272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Geromino opened this issue Apr 20, 2025 · 4 comments
Labels
ble workflow bug support issues that involve helping a user accomplish a task
Milestone

Comments

@Geromino
Copy link

CircuitPython version and board name

Adafruit CircuitPython 9.2.7 on a Feather nRF52840 Express.

Code/REPL

import asyncio
import time
import _bleio
import neopixel
import os
import board
import busio
import digitalio
import gc
import supervisor

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement


mac_address = 0
sw_version = "v1_2_9"

def parse_uname_output(uname_output):
    return {'version': uname_output[3] if len(uname_output) > 3 else None}

# uname_output-take only version 
uname_output = os.uname()
fw_version = parse_uname_output(uname_output)


def get_final_string():
    global mac_address
    mac_address = str(_bleio.adapter.address).replace("<Address ", "").replace(">", "")
    # print(f"[sensera app reads]: {mac_address}")
    easyg_device_name = "EasyG" + "".join(mac_address.split(":")[-4:])
    print("EasyG Name",easyg_device_name)
    return easyg_device_name


colors = {
    "white"     :(255, 255, 255), # white
    "off"       :(0,0,0),
    "red"       :(255,0,0),
    "green"     :(0, 255, 0),
    "yellow"    :(255, 255, 0),
    "orange"    :(255,165,0),
    "blue"      :(0, 0, 255)
    
}

pixels = neopixel.NeoPixel(board.NEOPIXEL,1,brightness=0.2)


ble = BLERadio()
ble.name = get_final_string()
advertisement = ProvideServicesAdvertisement()


easystatus = {
    "easystatus": {
        1: "TBD",       #Heart_rate_monitor
        2: 3,           #Vibration_pattern
        3: 2,           #Suction_intensity
        4: 3,           #Vibration_intensity
        5: 0,           #External_target_lubrication
        6: 0,           #Internal_target_lubrication
        7: 25.0,        #temperature_external_target
        8: 25.3,        #temperature_internal_target
        9: 25,          #External_heater_T
        10: 25,         #Internal_heater_T
        11: 0,          #Pressure_sensor_L
        12: 0,          #Pressure_sensor_R
        13: mac_address,#MAC
        14: False,      #Sensera_Status
        15: None,       #battery_level
        16: None        #charge_status
    }
}

easystatus1 = {
    "easystatus1": {
        17: fw_version,
        18: sw_version,
        19: None,       #wifi_version()
        20: "internal", #choosen_pump: "internal"/"external"/"both"
        21: 0,          #power_button_count (1-8)
        22: 0,          #drop_button_count (1/0)
        23: 0, #touch_pin1.raw_value
        24: 0, #touch_pin2.raw_value 
        25: 0, #gyro
        26: 0, #Accelerometer
        27: 0,  #System_Current
        28: 0, # touch1_average
        29: 0, # touch2_average
        30: 0 # current_avg_cap
        }
}


if ble.advertising:
    print("Auto-advertising detected. Stopping it...")
    ble.stop_advertising()
    
def log_event(message):
    try:
        with open("/log.txt", "a") as f:
            timestamp = time.monotonic()
            f.write(f"[{timestamp:.1f}] {message}\n")
    except Exception as e:
        print(f"Failed to write to log: {e}")    

async def ble_handler():
    toggle_debug = True
    while True:
        log_event("ble_handler: Top of loop")
        gc.collect()
        print("Free mem:", gc.mem_free(), "Allocated:", gc.mem_alloc())
        
        if not ble.connected and not ble.advertising:
            ble.stop_advertising()
            print("stop Advertising...")
            ble.start_advertising(advertisement)
            print("start Advertising...")
            #await sensera_debug_log("start Advertising")

        if ble.connected:
            conn = ble.connections[0]
            print("Connected to central!", conn)

            while conn.connected:               
                #device_cloud_commands = senserainfo.settings
                #senserainfo.easygstatus = easystatus
                #senserainfo.easygstatus1 = easystatus1
                    
                log_event("keep alive ble_handler device conected")
                print("keep alive ble_handler device conected")
                if toggle_debug :
                    pixels[0] = colors["orange"]
                    print("orange on")
                else :
                    pixels[0] = colors["off"]
                    print("orange off")
                toggle_debug = not toggle_debug  
                await asyncio.sleep(0.5)  # Avoid busy-waiting

            print("Connection lost!")
            #await sensera_debug_log("Connection lost")
            ble.stop_advertising()
            
        if toggle_debug :
            pixels[0] = colors["green"]
            print("green on")
        else :
            pixels[0] = colors["off"]
            print("orange off")
        toggle_debug = not toggle_debug
        
        log_event("keep alive ble_handler device not connected")
        print("keep alive ble_handler device not connected")
        await asyncio.sleep(2)

async def main():
    ble_task = asyncio.create_task(ble_handler())
    tasks = [ble_task]
    try:
        await asyncio.gather(*tasks)
    except Exception as e:
        print("Top-level crash:", e)
        log_event(f"Top-level exception: {e}")
        while True:
            pixels[0] = colors["red"]
            time.sleep(0.5)
            pixels[0] = colors["off"]
            time.sleep(0.2)
if __name__ == "__main__":
    reason = supervisor.runtime.run_reason
    log_event(f"Run reason: {reason}")
    log_event("System booted. Running main.")
    pixels[0] = colors["yellow"]
    time.sleep(0.5)
    pixels[0] = colors["off"]
    time.sleep(0.2)
    try:
        asyncio.run(main())
    except Exception as e:
        log_event(f"asyncio.run crash: {e}")

Behavior

Your code.py uses BLE only for advertising
The Glider app connects, and you can:
Create folders
Write files
After each file/folder operation (when running on battery, not USB), the device:

Performs: Run reason: supervisor.RunReason.AUTO_RELOAD

On reconnect (reopen app), the written data appears and is correct

Description

Your code.py uses BLE only for advertising
The Glider app connects, and you can:
Create folders
Write files
After each file/folder operation (when running on battery, not USB), the device:

Performs: Run reason: supervisor.RunReason.AUTO_RELOAD

On reconnect (reopen app), the written data appears and is correct

Additional information

No response

@Geromino Geromino added the bug label Apr 20, 2025
@Neradoc
Copy link

Neradoc commented Apr 20, 2025

Hi, Auto reloading when a file is changed via BLE/USB/Web workflow is the expected behavior, this allows rerunning when you make a change to the code, imported modules or files used by the code. You can disable autoreload in code via the supervisor module.

import supervisor
supervisor.runtime.autoreload = False

From the issue you opened in the adafruit_ble_file_transfer library (which is not required to use the BLE workflow), it seems that you want to use the file transfer library directly. I believe that for this to happen you need to implement the server side in your code. This example seems to have the basics but would need many changes to actually read and write to the filesystem (without causing a reload), also allowing you to restrict it to a subfolder for example. Then you would disable the BLE workflow with the supervisor module.

@Geromino
Copy link
Author

Geromino commented Apr 20, 2025

Thanks for the fast and detailed response!

I'd like to add some additional details to help clarify and fully explain the issue.

Our product is an IoT device, and we make extensive use of the BLE-based FTP service. Because of this, automatic reloads (auto-reload) after file transfers are problematic for our use case.

We have a dedicated folder where we store new recipes sent by the client. While we’re not currently using FTP as part of our OTA (Over-The-Air) update mechanism, it's still a critical part of how we interact with the device. Here's an overview of our current device tree:

graphql
Copy
Edit
📁 CircuitPython device
├── 📁 lib # Libraries used by CircuitPython scripts
│ ├── 📁 adafruit_ble # BLE support
│ ├── 📁 adafruit_lsm6ds # IMU driver
│ ├── 📁 adafruit_register # Register-based device helpers
│ ├── 📁 asyncio # Async I/O support
│ ├── 📄 adafruit_ble_file_transfer.mpy # BLE file transfer module
│ ├── 📄 adafruit_debouncer.mpy # Button debouncing (not used in code.py)
│ ├── 📄 adafruit_ina219.mpy # INA219 driver (not used in code.py)
│ ├── 📄 adafruit_thermistor.mpy # Thermistor driver (not used in code.py)
│ ├── 📄 adafruit_ticks.mpy # Tick-based timing utility
│ ├── 📄 neopixel.mpy # NeoPixel LED driver
├── 📁 mobile_recipe # Folder for client-submitted recipes
│ └── 📄 pleasure.json # Example recipe file written by client (e.g., Glider app)
├── 📄 ble_json_peripheral.py # BLE peripheral script (not used in code.py)
├── 📄 ble_json_service.py # BLE service definitions (not used in code.py)
├── 📄 boot.py # Boot script; disables USB drive for internal write access
├── 📄 boot_out.txt # Info about the board and CircuitPython version
├── 📄 button.py # Button handling logic
├── 📄 code.py # Main application logic
├── 📄 easyg_cloud_service.py # Custom cloud interface; transmits easystatus and easystatus1 (see lines 57–95)
├── 📄 log.txt # Runtime logs (BLE and main handler messages)
├── 📄 mgmt_settings.py # Settings management (not used in code.py)

my basic code without using mpy and server example only usage ftp buildin circuitpython fw

basic.txt

When I began testing FTP functionality, I discovered the Adafruit_CircuitPython_BLE_File_Transfer library and its examples. I tested the ble_file_transfer_stub_server.py example using the Glider app, and it worked as expected.

However, I wasn’t aware that the CircuitPython UF2 firmware includes a built-in FTP service by default. For about a month, our code.py included the full example-based FTP implementation from that library. Later, I noticed that BLE clients like nRF Connect were discovering two FTP services (see the image below).

Image

After realizing this, I removed the .mpy module and stopped using the stub server entirely — and surprisingly, FTP functionality continued to work. That’s when I understood that the FTP server is already implemented natively in CircuitPython 9.2.7.

This led to confusion: how can BLE clients distinguish between the two services when both are active (custom + built-in)? And more importantly, I confirmed that the auto-reload behavior is tied to the built-in FTP implementation, not my custom server in code.py.

❓ Follow-up Question:
I’ve noticed that auto-reload only happens when the device is powered by battery — it does not happen when connected via USB.

Do you know why this behavior differs between USB and battery operation?
It seems intentional, but I’d love to understand the reason or logic behind this design.

@Geromino
Copy link
Author

Follow-up Question:
I’ve noticed that auto-reload only happens when the device is powered by battery — it does not happen when connected via USB.

Do you know why this behavior differs between USB and battery operation?
It seems intentional, but I’d love to understand the reason or logic behind this design.

@Neradoc
Copy link

Neradoc commented Apr 21, 2025

When I began testing FTP functionality, I discovered the Adafruit_CircuitPython_BLE_File_Transfer library and its examples. I tested the ble_file_transfer_stub_server.py example using the Glider app, and it worked as expected.

That example is a "stub", it doesn't actually do anything and uses a fake file list (starting empty), there is no use of os.listdir(), os.stat(), open() or anything like that in it or in the library that would interact with the board's drive. That's why you need to implement the actual file operations for it to work, but it provides the structure where the commands are received from bluetooth and the result is sent back.
A fully functioning file server example that disables the BLE workflow and allows restricting to a sub directory could be interesting, but it's not there, sorry.

For now though, as I mentioned above, you can disable auto-reload.
That means that restarting the code after a change requires either a ctrl-C ctrl-D in the REPL or a reset. Or trigger a call to supervisor.reload() from code.

This led to confusion: how can BLE clients distinguish between the two services when both are active (custom + built-in)?

I assume the board just uses the builtin one and it takes precedence over the code.py one.
You can test by disabling the ble workflow supervisor.runtime.ble_workflow = False.

Follow-up Question: I’ve noticed that auto-reload only happens when the device is powered by battery — it does not happen when connected via USB.

I don't see that. When trying to change files via BLE while the board is connected to USB the file operations fail as expected (since the drive is only writable via USB) but it still triggers an auto-reload.

@tannewt tannewt added this to the Support milestone Apr 22, 2025
@tannewt tannewt added support issues that involve helping a user accomplish a task ble workflow labels Apr 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ble workflow bug support issues that involve helping a user accomplish a task
Projects
None yet
Development

No branches or pull requests

3 participants