roleoroleo / sonoff-hack

Custom firmware for Sonoff GK-200MP2B camera
GNU General Public License v3.0
200 stars 45 forks source link

Configure WiFi without eWelink #22

Closed meijerwynand closed 1 month ago

meijerwynand commented 3 years ago

Greetings,

Thanks for the work you have done on this project. Your project opens up the hacking possibilities for these devices.

I prefer to keep my devices "cloud-free" (as far as possible). I was digging around in the os-innards of the camera and I have made some headway in how to configure the WiFi particulars without submitting it via the eWelink app (sound method).

The short

I have been able to identify a number of changes that allows you to change the wireless credentials without online/app submission.

Files involved

Using meld I was able to compare the /mnt/mtd files of a factory reset and wifi-enabled only deployment

Sources

/mnt/mtd/ipc/app/script/dhcp.sh one of the main files that switch between the Wireless (ra0) and Ethernet (eth0) The command to run (modified) that calls for DHCP on ra0 udhcpc -v -a -b -i ra0 -x hostname:testing -p /var/dhcp.pid > /var/dhcp_debug.info

Challenge

I am able to make the changes to the database, the WPA file and add the DyVoiceRecog.bin which should start the WiFi, however this does not happen. I can see the connection coming in on my router, the WPA passes and then the device gets in IP, then it stays unable to route to host. The DHCP lease does not renew once expired. Once I connect the LAN cable then I get a DHCP and I can connect to the camera. I have tried making a number of changes and adding wpa_cli commands and the udhcpc commands to the /mnt/mmc/boot.sh file at various places, but to no avail.

Almost there

It feels like I am missing something simple and I would like to know if you have any thoughts. I am very sure the config changes I have found and made are close to a victory.

After the WiFi connection has failed and I plug the LAN cable in, when I run the commands from terminal all works 100%, my WPA succeeds, the device gets an IP, I am able to ssh to my root@wan.ip.add.ress, it just fails to boot with WiFi as the only connection method.

Trail and (t)errors

Prep for WiFi

Testing the WiFi changes

Reboot the device (LAN cable stays in) ssh root@lan.ip.add.ress

[root@GK]# wpa_cli -i ra0 status
wpa_state=DISCONNECTED
address=xx:xx:xx:xx:xx:xx

[root@GK]# wpa_cli -i ra0 disconnect
OK

[root@GK]# wpa_cli -i ra0 reconnect
OK

-- give a few seconds --

[root@GK]# wpa_cli -i ra0 status
bssid=xx:xx:xx:xx:xx:xx
freq=0
ssid=<your-ssid-will-show-here>
id=0
mode=station
pairwise_cipher=CCMP
group_cipher=CCMP
key_mgmt=WPA2-PSK
wpa_state=COMPLETED
address=xx:xx:xx:xx:xx:xx

The above indicates your WiFi is connected, but you will not have an IP address yet..

[root@GK]# udhcpc -a -b -i ra0 -x hostname:justtesting -p /var/dhcp.pid  > /var/dhcp.info
route: SIOCDELRT: No such process

[root@GK]# ip ad
..
3: ra0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 192.168.13.37/24 brd 192.168.13.255 scope global ra0
...

Connected!!

As you can see by now, the change in WiFi details are working and you can connect to your Access points

Sticky part

I just cant seem to get the WiFi to start correctly at boot. Not sure if it is config related, runlevel, or just pebkac

You inputs and expertise in the matter is highly appreciated and valued.

In closing

I trust feature like "Configure your WiFi from the web interface" would be a welcomed addition to the project.

roleoroleo commented 3 years ago

I did not go into this part very much because I am connected in lan. Do you know where the cam choose between wireless and cable? Is there a script or a config? Or it's automatic and when the cam starts, it tries to connect using lan and then wireless?

meijerwynand commented 3 years ago

I use grep to find whatever I am looking for, these are some of the observations.

Do you know where the cam choose between wireless and cable?

No,

Is there a script or a config?

Not too sure, I did see the sqlite file being changed and WiFi credentials being added there, however it also appears it can be override by using native cli commands (shown in previous post). The majority of my fiddling has lead me to think it's mainly scripts. There are 2 script directories:

Or it's automatic and when the cam starts, it tries to connect using lan and then wireless?

From the boot it appears to be a single interface. Either LAN or either WiFi, however, once logged in you can run both WiFi and LAN (even ethernet alias on eth0)

eth0      Link encap:Ethernet  HWaddr xx:xx:xx:xx:xx:xx
          inet addr:192.168.14.19  Bcast:192.168.14.255  Mask:255.255.255.0
...
eth0:0    Link encap:Ethernet  HWaddr xx:xx:xx:xx:xx:xx
          inet addr:192.168.14.55  Bcast:192.168.14.255  Mask:255.255.255.0
...
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
...
ra0       Link encap:Ethernet  HWaddr 68:B9:D3:93:AC:22  
          inet addr:192.168.13.37  Bcast:192.168.13.255  Mask:255.255.255.0
...

Interesting finds...

Conclusion

This is what I have added at the bottom of /mnt/mmc/sonoff-hack/script/system.sh

...
## Network hack
## Set static IP for backdoor entrance
/sbin/ifconfig eth0:0 192.168.14.55 up

## kill dhcp server 
pid=$(ps | grep udhcpd | grep -v grep | awk '{print $1}') && kill $pid || echo 'Nothing to kill'

## kill captive portal 
pid=$(ps | grep '/mnt/mtd/ipc/app/captive_server' | grep -v grep | awk '{print $1}') && kill $pid || echo 'Nothing to kill'

## kill ap
pid=$(ps | grep '/gm/bin/hostapd' | grep -v grep | awk '{print $1}') && kill $pid || echo 'Nothing to kill'

## kill dhcp
pid=$(ps | grep 'udhcpc' | grep -v grep | awk '{print $1}') && kill $pid || echo 'Nothing to kill'

## try again
/gm/bin/wpa_supplicant -Dwext -B -ira0 -c/mnt/mtd/ipc/cfg/wpa_supplicant.conf -P /var/pid_wpa_s_debug

killall udhcpd
/gm/bin/wpa_cli -ira0 reconnect
sleep 20
ifconfig eth0 0.0.0.0
/gm/bin/wpa_cli -ira0  status > /var/wpa_debug.info
killall udhcpc
udhcpc -v -a -b -i ra0 -x hostname:IPC-ZZZZ-XXX-TTTTT-WIFI -p /var/dhcp_debug.pid  > /var/dhcp_debug.info
meijerwynand commented 3 years ago

Preliminary success ... kind off..

WiFi has NOT been configured via the eWeLink app. You can boot the device and obtain WiFi access.

When WiFi is not configured, during boot it attempts to go into AP mode, creating a captive portal and its own DHCP server. After this boot completes, forcefully kill participating processes, and interface start wpa_supplicant - config in previous post and bring up interface and DHCP client

It takes bout ~1 min to boot and get WiFI connected with an IP

Not working...

Functional but not production ready code This file is a stanalone file (/mnt/mmc/sonoff-hack/script/network.sh) being called at the bottom of /mnt/mmc/sonoff-hack/script/system.sh

#!/bin/ash
## stop and kill 
LOG_FILE=/tmp/wifi_debug
> ${LOG_FILE}

ps >>  ${LOG_FILE}
echo "--------------------------------------------------------------------------">>  ${LOG_FILE}
echo "killing processes and starting wpa_supplicant">>  ${LOG_FILE}
killall captive_server
killall hostapd
killall udhcpd

wpa_supplicant -Dwext -B -ira0 -c/mnt/mtd/ipc/cfg/wpa_supplicant.conf -P /var/pid_wpa_s

echo "--------------------------------------------------------------------------">>  ${LOG_FILE}
ps >>  ${LOG_FILE}
ifconfig ra0 >> ${LOG_FILE} 2>&1 

## disconnect wifi
echo "`date` : wpa_cli -i ra0 disconnect" >> ${LOG_FILE}
wpa_cli -i ra0 disconnect >> ${LOG_FILE} 2>&1

## stop interface
echo "`date` : ifconfig ra0 down" >> ${LOG_FILE}
ifconfig ra0 down >> ${LOG_FILE} 2>&1 

## kill any running based on pid file 
UDHCPC_PID="/var/dhcp.ra0.pid"
if [[ -f "${UDHCPC_PID}" ]]; then 
  echo "`date` : removing pid ${UDHCPC_PID}"  >> ${LOG_FILE}
  kill `cat ${UDHCPC_PID}`
  rm ${UDHCPC_PID}
fi

## kill wifi done... 

## bring up interface 
echo "`date` : ifconfig ra0 up" >> ${LOG_FILE}
ifconfig ra0 up >> ${LOG_FILE} 2>&1
ifconfig ra0 >> ${LOG_FILE} 2>&1 
sleep 3
wpa_cli -i ra0 reconfigure >> ${LOG_FILE} 2>&1

## bring up wifi
echo "`date` : wpa_cli -i ra0 reconnect" >> ${LOG_FILE} 2>&1
wpa_cli -i ra0 reconnect >> ${LOG_FILE} 2>&1

## attempt a dhcp ip
udhcpc -i ra0 -b -S -x hostname:123-123 --tryagain 45 -p ${UDHCPC_PID} >> ${LOG_FILE} 2>&1
shafr commented 3 years ago

Well I pretty much have the same issue - my password is 60+ characters long and i'm not able to set it up using sound prompt. I had to use cable, but i'm very happy if I would be able to set it somehow...

kruzer commented 2 years ago

Preliminary success ... kind off..

I is working for me. I wasn't able to get the wifi connection with sound pairing, but now it is connected without ethernet and everything seems to function properly. Steps to reproduce:

  1. Boot with sonoff-hack
  2. ssh to your camera
  3. edit wifi config - put your sid and psk here: vi /mnt/mtd/ipc/cfg/wpa_supplicant.conf
  4. edit and paste the network script from @meijerwynand vi /mnt/mmc/sonoff-hack/script/network.sh
  5. edit system.sh vi /mnt/mmc/sonoff-hack/script/system.sh call the network script after the DISABLE_CLOUD part, and move the "Restart ptz driver" after this call. My system.sh after the change:
    
    #!/bin/sh

CONF_FILE="etc/system.conf"

SONOFF_HACK_PREFIX="/mnt/mmc/sonoff-hack" SONOFF_HACK_UPGRADE_PATH="/mnt/mmc/.fw_upgrade"

SONOFF_HACK_VER=$(cat /mnt/mmc/sonoff-hack/version) MODEL=$(cat /mnt/mtd/ipc/cfg/config_cst.cfg | grep model | cut -d'=' -f2 | cut -d'"' -f2) DEVICE_ID=$(cat /mnt/mtd/ipc/cfg/colink.conf | grep devid | cut -d'=' -f2 | cut -d'"' -f2)

get_config() { key=$1 grep -w $1 $SONOFF_HACK_PREFIX/$CONF_FILE | cut -d "=" -f2 }

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/mmc/sonoff-hack/lib export PATH=$PATH:/mnt/mmc/sonoff-hack/bin:/mnt/mmc/sonoff-hack/sbin:/mnt/mmc/sonoff-hack/usr/bin:/mnt/mmc/sonoff-hack/usr/sbin

touch /tmp/httpd.conf

if [ -f $SONOFF_HACK_UPGRADE_PATH/sonoff-hack/fw_upgrade_in_progress ]; then echo "#!/bin/sh" > /tmp/fw_upgrade_2p.sh echo "# Complete fw upgrade and restore configuration" >> /tmp/fw_upgrade_2p.sh echo "sleep 1" >> /tmp/fw_upgrade_2p.sh echo "cd $SONOFF_HACK_UPGRADE_PATH" >> /tmp/fw_upgrade_2p.sh echo "cp -rf * .." >> /tmp/fw_upgrade_2p.sh echo "cd .." >> /tmp/fw_upgrade_2p.sh echo "rm -rf $SONOFF_HACK_UPGRADE_PATH" >> /tmp/fw_upgrade_2p.sh echo "rm $SONOFF_HACK_PREFIX/fw_upgrade_in_progress" >> /tmp/fw_upgrade_2p.sh echo "sync" >> /tmp/fw_upgrade_2p.sh echo "sync" >> /tmp/fw_upgrade_2p.sh echo "sync" >> /tmp/fw_upgrade_2p.sh echo "reboot" >> /tmp/fw_upgrade_2p.sh sh /tmp/fw_upgrade_2p.sh exit fi

$SONOFF_HACK_PREFIX/script/check_conf.sh

cp -f $SONOFF_HACK_PREFIX/etc/hostname /etc/hostname hostname -F /etc/hostname export TZ=$(get_config TIMEZONE)

if [[ $(get_config SWAP_FILE) == "yes" ]] ; then SD_PRESENT=$(mount | grep mmc | grep -c ^) if [[ $SD_PRESENT -eq 1 ]]; then if [[ -f /mnt/mmc/swapfile ]]; then swapon /mnt/mmc/swapfile else dd if=/dev/zero of=/mnt/mmc/swapfile bs=1M count=64 chmod 0600 /mnt/mmc/swapfile mkswap /mnt/mmc/swapfile swapon /mnt/mmc/swapfile fi fi fi

Create hack user if doesn't exist

HACK_USER=$(sqlite3 /mnt/mtd/db/ipcsys.db "select count(*) from t_user where C_UserID=10101;") if [[ $HACK_USER -eq 0 ]]; then sqlite3 /mnt/mtd/db/ipcsys.db "insert into t_user (C_UserID, c_role_id, C_UserName, C_PassWord) values (10101, 1, 'hack', 'hack');" fi

if [[ x$(get_config USERNAME) != "x" ]] ; then USERNAME=$(get_config USERNAME) PASSWORD=$(get_config PASSWORD) RTSP_USERPWD="" ONVIF_USERPWD="--user $USERNAME --password $PASSWORD" echo "/:$USERNAME:$PASSWORD" > /tmp/httpd.conf else RTSP_USERPWD="hack:hack@" fi

cp -f $SONOFF_HACK_PREFIX/etc/passwd /etc/passwd cp -f $SONOFF_HACK_PREFIX/etc/shadow /etc/shadow PASSWORD_MD5='$1$$qRPK7m23GJusamGpoGLby/' if [[ x$(get_config SSH_PASSWORD) != "x" ]] ; then SSH_PASSWORD=$(get_config SSH_PASSWORD) PASSWORD_MD5="$(echo "${SSH_PASSWORD}" | mkpasswd --method=MD5 --stdin)" fi CUR_PASSWORD_MD5=$(awk -F":" '$1 == "root" { print $2 } ' /etc/shadow) if [[ x$CUR_PASSWORD_MD5 != x$PASSWORD_MD5 ]] ; then sed -i 's|^(root:)[^:]*:|root:'${PASSWORD_MD5}':|g' "/etc/shadow" fi

case $(get_config ONVIF_PORT) in ''|[!0-9]) ONVIF_PORT=1000 ;; ) ONVIF_PORT=$(get_config ONVIF_PORT) ;; esac case $(get_config HTTPD_PORT) in ''|[!0-9]) HTTPD_PORT=80 ;; ) HTTPD_PORT=$(get_config HTTPD_PORT) ;; esac

if [[ $(get_config DISABLE_CLOUD) == "yes" ]] ; then

Add forbidden domains

echo "127.0.0.1               eu-dispd.coolkit.cc" >> /etc/hosts
echo "127.0.0.1               eu-api.coolkit.cn" >> /etc/hosts
echo "127.0.0.1               testapi.coolkit.cn" >> /etc/hosts
echo "127.0.0.1               push.iotcare.cn" >> /etc/hosts
echo "127.0.0.1               www.iotcare.cn" >> /etc/hosts
echo "127.0.0.1               alive.hapsee.cn" >> /etc/hosts
echo "127.0.0.1               upgrade.hapsee.cn" >> /etc/hosts
echo "127.0.0.1               hapseemate.cn" >> /etc/hosts
echo "127.0.0.1               iotgo.iteadstudio.com" >> /etc/hosts
echo "127.0.0.1               baidu.com" >> /etc/hosts
echo "127.0.0.1               sina.com" >> /etc/hosts

# Add forbidden IPs
ip route add prohibit 13.52.12.176/32

# Kill ProcessGuard and iot processes
touch /tmp/bProcessGuardExit
killall colinkwtg.sh
killall colink.sh
killall colink
killall IOTCare

else umount /mnt/mtd/ipc/app/colink rm /tmp/colink fi

if [ -f "/mnt/mmc/sonoff-hack/script/network.sh" ]; then /mnt/mmc/sonoff-hack/script/network.sh fi

Restart ptz driver

rmmod ptz_drv.ko sleep 1 insmod /mnt/mtd/ipc//app/drive/ptz_drv.ko factory="Links" AutoRun=1 Horizontal=3500 Vertical=900 if [[ $(get_config PTZ_PRESET_BOOT) != "default" ]] ; then (sleep 20 && /mnt/mmc/sonoff-hack/bin/ptz -a go_preset -f $SONOFF_HACK_PREFIX/etc/ptz_presets.conf -n $(get_config PTZ_PRESET_BOOT)) & fi

if [[ $(get_config HTTPD) == "yes" ]] ; then mkdir -p /mnt/mmc/alarm_record mkdir -p /mnt/mmc/sonoff-hack/www/alarm_record mount --bind /mnt/mmc/alarm_record /mnt/mmc/sonoff-hack/www/alarm_record httpd -p $HTTPD_PORT -h $SONOFF_HACK_PREFIX/www/ -c /tmp/httpd.conf fi

if [[ $(get_config TELNETD) == "no" ]] ; then killall telnetd fi

if [[ $(get_config FTPD) == "yes" ]] ; then if [[ $(get_config BUSYBOX_FTPD) == "yes" ]] ; then tcpsvd -vE 0.0.0.0 21 ftpd -w & else pure-ftpd -B fi fi

if [[ $(get_config SSHD) == "yes" ]] ; then mkdir -p $SONOFF_HACK_PREFIX/etc/dropbear if [ ! -f $SONOFF_HACK_PREFIX/etc/dropbear/dropbear_ecdsa_host_key ]; then dropbearkey -t ecdsa -f /tmp/dropbear_ecdsa_host_key mv /tmp/dropbear_ecdsa_host_key $SONOFF_HACK_PREFIX/etc/dropbear/ fi

Restore keys

mkdir -p /etc/dropbear
cp -f $SONOFF_HACK_PREFIX/etc/dropbear/* /etc/dropbear/
chmod 0600 /etc/dropbear/*
dropbear -R

fi

if [[ $(get_config NTPD) == "yes" ]] ; then

Wait until all the other processes have been initialized

sleep 5 && ntpd -p $(get_config NTP_SERVER) &

fi

if [[ $(get_config MQTT) == "yes" ]] ; then $SONOFF_HACK_PREFIX/bin/mqtt-sonoff & fi

if [[ $ONVIF_PORT != "80" ]] ; then D_ONVIF_PORT=:$ONVIF_PORT fi

if [[ $HTTPD_PORT != "80" ]] ; then D_HTTPD_PORT=:$HTTPD_PORT fi

if [[ $(get_config ONVIF) == "yes" ]] ; then if [[ $(get_config ONVIF_NETIF) == "ra0" ]] ; then ONVIF_NETIF="ra0" else ONVIF_NETIF="eth0" fi

ONVIF_PROFILE_1="--name Profile_1 --width 640 --height 360 --url rtsp://$RTSP_USERPWD%s/av_stream/ch1 --snapurl http://%s$D_HTTPD_PORT/cgi-bin/snapshot.sh --type H264"
ONVIF_PROFILE_0="--name Profile_0 --width 1920 --height 1080 --url rtsp://$RTSP_USERPWD%s/av_stream/ch0 --snapurl http://%s$D_HTTPD_PORT/cgi-bin/snapshot.sh --type H264"

onvif_srvd --pid_file /var/run/onvif_srvd.pid --model "Sonoff Hack" --manufacturer "Sonoff" --firmware_ver "$SONOFF_HACK_VER" --hardware_id $MODEL --serial_num $DEVICE_ID --ifs $ONVIF_NETIF --port $ONVIF_PORT --scope onvif://www.onvif.org/Profile/S $ONVIF_PROFILE_0 $ONVIF_PROFILE_1 $ONVIF_USERPWD --ptz --move_left "/mnt/mmc/sonoff-hack/bin/ptz -a left" --move_right "/mnt/mmc/sonoff-hack/bin/ptz -a right" --move_up "/mnt/mmc/sonoff-hack/bin/ptz -a up" --move_down "/mnt/mmc/sonoff-hack/bin/ptz -a down" --move_stop "/mnt/mmc/sonoff-hack/bin/ptz -a stop" --move_preset "/mnt/mmc/sonoff-hack/bin/ptz -f /mnt/mmc/sonoff-hack/etc/ptz_presets.conf -a go_preset -n %t" --set_preset "/mnt/mmc/sonoff-hack/bin/ptz -f /mnt/mmc/sonoff-hack/etc/ptz_presets.conf -a set_preset -e %n -n %t"
if [[ $(get_config ONVIF_WSDD) == "yes" ]] ; then
    wsdd --pid_file /var/run/wsdd.pid --if_name $ONVIF_NETIF --type tdn:NetworkVideoTransmitter --xaddr http://%s$D_ONVIF_PORT --scope "onvif://www.onvif.org/name/Unknown onvif://www.onvif.org/Profile/Streaming"
fi

fi

Add crontab

CRONTAB=$(get_config CRONTAB) FREE_SPACE=$(get_config FREE_SPACE) if [ ! -z "$CRONTAB" ] || [ "$FREE_SPACE" != "0" ] ; then mkdir -p /var/spool/cron/crontabs/

if [ ! -z "$CRONTAB" ]; then
    echo "$CRONTAB" > /var/spool/cron/crontabs/root
fi
if [ "$FREE_SPACE" != "0" ]; then
    echo "0 * * * * /mnt/mmc/sonoff-hack/script/clean_records.sh $FREE_SPACE" > /var/spool/cron/crontabs/root
fi

/usr/sbin/crond -c /var/spool/cron/crontabs/

fi

if [[ $(get_config FTP_UPLOAD) == "yes" ]] ; then /mnt/mmc/sonoff-hack/script/ftppush.sh start & fi

if [ -f "/mnt/mmc/startup.sh" ]; then /mnt/mmc/startup.sh fi

kruzer commented 2 years ago

Another solution i found useful:

  1. Temporary change your WIFI password in Access Point to a short-simple one.
  2. Pair with eWeLink
  3. Boot with sonoff-hack and ssh to camera
  4. Change the short password in /mnt/mtd/ipc/cfg/wpa_supplicant.conf to the appropriate one in line:
    psk="<Real Password>"
  5. Update it in db:
    /mnt/mmc/sonoff-hack/bin/sqlite3 /mnt/mtd/db/ipcsys.db "update t_sys_param set c_param_value='<Real Password>' where c_param_name='wf_key';"
  6. Recover your real password in AP and reboot the camera
shafr commented 2 years ago

Thanks for manual @kruzer . I created a hotspot wifi on my laptop & followed your instructions. I would also add - if you want to switch wifi name - param is wf_ssid. If you want to check param values:

/mnt/mmc/sonoff-hack/bin/sqlite3 /mnt/mtd/db/ipcsys.db "select * from t_sys_param" | grep wf
jezzaaa commented 2 years ago

Presumably, the wpa_supplicant.conf file is populated from the database at boot time, so I could edit the sqlite db file only, then reboot.

I'd love for the sonoff-hack code to provide an interface to setting this.

jezzaaa commented 2 years ago

Thanks for manual @kruzer . I created a hotspot wifi on my laptop & followed your instructions. I would also add - if you want to switch wifi name - param is wf_ssid. If you want to check param values:

/mnt/mmc/sonoff-hack/bin/sqlite3 /mnt/mtd/db/ipcsys.db "select * from t_sys_param" | grep wf

Or, use SQL wildcards instead of grep:

/mnt/mmc/sonoff-hack/bin/sqlite3 /mnt/mtd/db/ipcsys.db "select * from t_sys_param where c_param_name like 'wf_%'"
roleoroleo commented 2 years ago

I'd love for the sonoff-hack code to provide an interface to setting this.

I think I could add it.

roleoroleo commented 2 years ago

Check this commit: https://github.com/roleoroleo/sonoff-hack/commit/32c2f55c248ecce3f650d82df51a354d60c55e66

jezzaaa commented 2 years ago

Check this commit: 32c2f55

Awesome, thanks @roleoroleo .

I don't know how to apply the commit without manually editing files. So I'll wait until the next version bump, so I can use the "Upgrade Firmware" button. Unless there's a clean way to apply this, that I'm not aware of?

github-actions[bot] commented 2 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.