4:
675 | ### update the check and conn timer
676 | #VWORK_LIST[fworkout][0] = ftimenow
677 | print2('fworker tried a few times, delaying')
678 | # this happens also too often, not logging
679 | ### v51_25 adding 2 minutes
680 | ### v52_17 shortening to 10 sec, as delay time is added elsewhere
681 | ### v52_42 add more, as there were 5 retries
682 | try:
683 | #ble.gap_disconnect(VWORK_LIST[fworkout][2])
684 | #ble.gap_disconnect(0)
685 | #ble.gap_disconnect(1)
686 | print2('fworker connect cleanup pending ', fworkout)
687 | fdisconnect(ble, 0)
688 | fdisconnect(ble, 1)
689 | VWORK_LIST[fworkout][1] = ftimenow + errordelay
690 | VWORK_LIST[fworkout][3] = 8
691 | VWORK_LIST[fworkout][5] = None
692 | except:
693 | print2('fworker connect cleanup failed ', fworkout)
694 | pass
695 | if len(VWORK_LIST[fworkout][4]) > 4: ### v51_25 was 10 is 4, how many tasks are in the list
696 | ### too much work, removing
697 | VWORK_LIST[fworkout][4].pop(0)
698 | #gc.collect()
699 | return
700 | ### no return in main if, as there might be some work to do
701 | ###
702 | ### if the above is fine, just move forward
703 | ###
704 | if VWORK_LIST[fworkout][3] not in [7, 8, 77]:
705 | # v53_21 added 77 so that waiting connection will not be cleaned up
706 | print2('fworker maybe working')
707 | ### v52_02 clean up in case of stuck device
708 | gc.collect()
709 | # TODO
710 | # if maybe working for more than 10 sec, then disconnect
711 | # it waits until next round
712 | # if ftimenow - VWORK_LIST[fworkout][1] > 10:
713 | if ftimenow - VWORK_LIST[fworkout][1] > ( VGLOB['delaywork'] * len(VWORK_LIST) ) + 5:
714 | try:
715 | #ble.gap_disconnect(VWORK_LIST[fworkout][2])
716 | fdisconnect(ble, VWORK_LIST[fworkout][2])
717 | except Exception as e:
718 | print2('fworker disconn warn 3 ', e)
719 | #VWORK_LIST[fworkout][1] = ftimenow + 120 ### v51_24 added 2 minutes wait
720 | VWORK_LIST[fworkout][2] = None
721 | VWORK_LIST[fworkout][3] = 8 # v53_23
722 | ### v52_02 - delaying anyway
723 | ### v51_24 added 2 minutes wait, v52_04 - 3 minutes, v52_10 changed to 1 min, v52_17 delay changed to 20 sec
724 | VWORK_LIST[fworkout][1] = ftimenow + errordelay
725 | print2("fworker connection too long")
726 | # return
727 | ###
728 | # v53_17 added new
729 | elif VWORK_LIST[fworkout][3] == 77:
730 | #
731 | if type( VWORK_LIST[fworkout][5] ) is str: # from == to is
732 | VWORK_LIST[fworkout][5] = None
733 | # longer waiting times
734 | # do not update time here, as the connection may fail
735 | VWORK_LIST[fworkout][5] = int(VWORK_LIST[fworkout][5] or 0) + 1
736 | print2("fworker waiting for connection", VWORK_LIST[fworkout][5] )
737 | #
738 | # and VGLOB['status'] in [5, 8]
739 | elif VWORK_LIST[fworkout][3] == 8 and ftimenow > VWORK_LIST[fworkout][1]: ### v51_25 added time check
740 | ### finally, all above is fine, and work is to be done, so connect first
741 | print2("fworker connecting")
742 | ### v52_15 cleanup the value, as the detection is postponed already
743 | if type( VWORK_LIST[fworkout][5] ) is str: # from == to is
744 | VWORK_LIST[fworkout][5] = None
745 | # longer waiting times
746 | # do not update time here, as the connection may fail
747 | #VWORK_LIST[fworkout][5] = int(VWORK_LIST[fworkout][5] or 0) + 1 # v53_21
748 | VWORK_LIST[fworkout][3] = 77 # v53_14 added status change !!! to avoid reconnecting
749 | VGLOB['status'] = 77
750 | try:
751 | # increased connection time
752 | #ble.gap_connect( 0, fcode_addr(fworkout), 10000 )
753 | ### GAP CONNECT
754 | VWORK_LIST[fworkout][5] = 1 # v53_21
755 | #ble.gap_connect( 0, fcode_addr(fworkout), int( 3 * VGLOB['delaywork'] * 1000 ) )
756 | ble.gap_connect( 0, fcode_addr(fworkout), int( 4 * VGLOB['delaywork'] * 1000 ) ) # v53_16 longer time 4*
757 | ## v52_39 defined error exceptions added
758 | ## v52_40 improved error detection
759 | # other error [Errno 19] ENODEV
760 | except Exception as e:
761 | print2('fworker conn warn ', e, fworkout)
762 | #print2(e.__class__.__name__)
763 | if str(e).split("] ",1)[1] in ('EIO'):
764 | try:
765 | #ble.gap_disconnect(0) # v53_21 changed from 0, to an actual value
766 | #ble.gap_disconnect(VWORK_LIST[fworkout][2])
767 | #ble.gap_disconnect(1)
768 | fdisconnect(ble, 0)
769 | fdisconnect(ble, 1)
770 | VWORK_LIST[fworkout][1] = ftimenow + errordelay
771 | VWORK_LIST[fworkout][3] = 8 # v53_15
772 | VWORK_LIST[fworkout][5] = None # v53_21
773 | except:
774 | print2('fworker connect cleanup failed ', fworkout)
775 | pass
776 | if str(e).split("] ",1)[1] in ('EALREADY'):
777 | print2('fworker connect already connected ', fworkout)
778 | # this appears every time a single connection fails, not necessary to report
779 | print2("fworker connection timeout")
780 | fpostpone()
781 | #
782 | ###
783 | ### assuming connected, so work
784 | elif VWORK_LIST[fworkout][3] in [7, 18]:
785 | print2("fworker connected, and handle available, send work")
786 | # stop scan if any
787 | fstopscan(ble)
788 | #
789 | if len( VWORK_LIST[fworkout][4] ) == 0:
790 | ### this deletes position 5 if working, is it fine ?
791 | ### clean up [5] only if no work to be done, v53_04
792 | VWORK_LIST[fworkout][5] = None
793 | ###
794 | print2("fworker, connected, but no work, disconnect")
795 | #ble.gap_disconnect(VWORK_LIST[fworkout][2])
796 | fdisconnect(ble, VWORK_LIST[fworkout][2])
797 | ### v52_01 - cleaning
798 | gc.collect()
799 | return
800 | # maybe not update, to do whole work in one run
801 | # select first from the work list
802 | worka = str(VWORK_LIST[fworkout][4][0]).strip().split(' ')
803 | for iii in range(max(0, 2 - len(worka))):
804 | worka.append('')
805 | ### 43 no thread
806 | _thread.start_new_thread(fble_write, (fworkout, worka[0], worka[1]))
807 | #fble_write(fworkout, worka[0], worka[1])
808 | ###
809 | ### some other situation happened
810 | else:
811 | #mqtth.check_msg()
812 | #fscan(0) ### v51_26 added, if nothing happens then scan
813 | print2('fworker unexpected situation - no work, nothing done')
814 | gc.collect()
815 | return
816 |
817 | #-####
818 | #-####
819 | # -#### webpage generating function
820 |
821 | def fwebpage() -> str:
822 | html_in = ""
823 | for addr, val in VWORK_LIST.items():
824 | html_in += str(addr) + " - " + str(val[4]) + "\n"
825 | # generate table
826 | # generate rest of html
827 | #""" + str(VGLOB) + """
828 | html = """
829 |
830 |
831 | EQ3 controller
832 |
833 |
834 |
835 |
836 |
837 | EQ3 controller
838 | By Dr. JJ on ESP32 and micropython.
839 | GitHub page.
840 | System
841 | Last work done on: """ + str(fnow(VGLOB['timelast'])) + """
842 | Last change: """ + str(fnow()) + """
843 | Boot: """ + str(fnow(VGLOB['timeup'])) + """
844 | Location: """ + str(CONFIG2['mqtt_usr']) + """
845 | IP: """ + str(station.ifconfig()[0]) + """
846 | Version: """ + str(CONFIG2['__version__']) + """
847 | Links:
848 | List of devices
849 | Update OTA
850 | System info
851 | Add webrepl - Webrepl console (pass: 1234)
852 |
853 | Publish MQTT autodiscovery
854 | Rescan devices
855 | Reset
856 | List of contacted devices
857 |
858 | """ + str(html_in) + """
859 |
860 | ---
861 |
862 | """
863 | del html_in
864 | html = html.encode('ascii')
865 | #html = html.encode('latin-1')
866 | print2('fweb generating page')
867 | #
868 | #gc.collect()
869 | #
870 | return( html )
871 |
872 | #-####
873 | #-####
874 | # -#### webpage loop function
875 | # -#### was based on socket, but now on async is more responsive and less consuming
876 |
877 | async def loop_web(reader, writer) -> None:
878 | flood = 0
879 | #
880 | #if VGLOB['status'] in [17, 18]: # v53_29 if working, skip page to avoid flood
881 | # detect and avoid flood earlier, v53_06, changed from 20000 to 45000
882 | if gc.mem_free() < 25000 or VGLOB['flood'] == 1:#
883 | print2('fweb page flood 1')
884 | flood = 1
885 | #return
886 | #global VGLOB
887 | VGLOB['flood'] = 1
888 | # waiting for input
889 | #recv = await reader.read(64)
890 | ### timeout here ? # v53_10
891 | # reader, writer = yield from asyncio.wait_for(fut, timeout=3)
892 | gc.collect()
893 | #global ERRORLOG
894 | try:
895 | #recvtmp = recv.decode()
896 | if flood == 0:
897 | #=await asyncio.sleep(0.1)
898 | recv = yield from reader.read(64)
899 | requesttype = recv.decode()[0:3]
900 | requestfull = recv.decode().split('\r')[0].split(' ')[1].split('?')
901 | #requestfull = requestfull # [4:-6]
902 | #recv2 = await reader.read()
903 | #print2( recv2.decode() )
904 | else:
905 | requestfull = ['/flood']
906 | except Exception as e:
907 | # if request invalid or malformed
908 | print2('fweb page request warn ', e)
909 | requestfull = ['/']
910 | # continue
911 | # ?
912 | #
913 | print2('fweb serving page ', requestfull)
914 | #global VGLOB
915 | #global VSCAN_LIST
916 | request = requestfull[0]
917 | #print2(request, requestfull)
918 | requestval = ''
919 | vwebpage = b''
920 | resp = b''
921 | #timer2 = time.ticks_ms()
922 | #gc.collect()
923 | if len(requestfull) == 2:
924 | requestval = requestfull[1]
925 | #
926 | if request == "/":
927 | vwebpage = fwebpage()
928 | # Server-Timing: text;dur=""" + str(time.ticks_ms() - timer2) + """, req;dur=""" + str(timer2 - timer1) + """
929 | header = """HTTP/1.1 200 OK
930 | Content-Type: text/html
931 | Content-Length: """ + str(len(vwebpage)) + """
932 | Connection: close
933 | """
934 | #conn.sendall(header + "\r\n" + vwebpage)
935 | await writer.awrite(header + "\r\n")
936 | # gc.collect()
937 | # INFO
938 | # continue
939 | #####
940 | #####
941 | elif request == "/flood":
942 | header = """HTTP/1.1 429 Too Many Requests
943 | Retry-After: 3
944 | Content-Type: text/plain
945 | Content-Length: 27
946 | Connection: close
947 |
948 | flood, retry in 1-2 seconds
949 | """
950 | await writer.awrite(header + "\r\n")
951 | # # gc.collect()
952 | elif request == "/list":
953 | # stop scan if any
954 | fstopscan(ble)
955 | # collect
956 | gc.collect()
957 | #
958 | vwebpage = '\n'
959 | vwebpage += '
\n'
969 | else:
970 | vwebpage += 'empty list\n'
971 | ###
972 | vwebpage += '\n'
973 | vwebpage += '
MAC remove white listed\n'
974 | vwebpage += '
\n'
981 | vwebpage += '\n'
982 | ###
983 | header = """HTTP/1.1 200 OK
984 | Content-Type: text/html
985 | Content-Length: """ + str(len(vwebpage)) + """
986 | Connection: close
987 | """
988 | #conn.sendall(header + "\r\n" + vwebpage)
989 | await writer.awrite(header + "\r\n")
990 | # INFO
991 | # await writer.awrite(vwebpage)
992 | # conn.close()
993 | #####
994 | ##### do all the work here
995 | #####
996 | elif request == "/todo":
997 | #maca=E1:CF:E3:5A:04:28&type=climate&work=add
998 | #if requesttype != "POS":
999 | # #break
1000 | # return
1001 | recv2 = yield from reader.read(2000)
1002 | recv3 = recv2.decode().split('\r')[-1].replace('%3A', ':').strip().split('&')
1003 | print2('fweb', recv3)
1004 | recv3w = recv3[-1].split('=')[1]
1005 | recv3m = recv3[0].split('=')[1]
1006 | #vwork['0'] = 'scan'
1007 | header = """HTTP/1.1 200 OK
1008 | Content-Type: text/html
1009 | Content-Length: 39
1010 | Connection: close
1011 |
1012 | Work -> """ + str( recv3w ) + """, mac -> """ + str( recv3m ) + """.
1013 | """
1014 | #conn.sendall(header + "\r\n" + vwebpage)
1015 | await writer.awrite(header + "\r\n")
1016 | # addr, time, handle, work, status
1017 | # VSCAN_LIST[str(requestval)][0]
1018 | if recv3w == "del":
1019 | #requestval = str(requestval)[0:17]
1020 | del VWORK_LIST[str(recv3m)]
1021 | elif recv3w == "add":
1022 | VWORK_LIST[str(recv3m)] = [None, time.time(), None, 8, [], None]
1023 | else:
1024 | return
1025 | filewl = open('wl.txt', 'w')
1026 | filewl.write( str(VWORK_LIST) )
1027 | filewl.close()
1028 | #####
1029 | #####
1030 | #####
1031 | elif request == "/deldo":
1032 | header = """HTTP/1.1 302 Found
1033 | Content-Length: 0
1034 | Location: /info
1035 | Connection: close
1036 | """
1037 | # Connection: close
1038 | if requestval != '':
1039 | try:
1040 | os.remove(requestval)
1041 | except Exception as e:
1042 | # try to remove file, if fail no panic
1043 | print2('fweb deldo file does not exist ', e)
1044 | #pass
1045 | # conn.sendall(header)
1046 | await writer.awrite(header + "\r\n")
1047 | # await writer.awrite(vwebpage)
1048 | #####
1049 | #####
1050 | elif request == "/info":
1051 | #
1052 | if machine.reset_cause() == 0:
1053 | reset_cause = "PWRON_RESET"
1054 | elif machine.reset_cause() == 1:
1055 | reset_cause = "HARD_RESET"
1056 | elif machine.reset_cause() == 2:
1057 | reset_cause = "WDT_RESET"
1058 | elif machine.reset_cause() == 3:
1059 | reset_cause = "DEEPSLEEP_RESET"
1060 | elif machine.reset_cause() == 4:
1061 | reset_cause = "SOFT_RESET"
1062 | elif machine.reset_cause() == 5:
1063 | reset_cause = "BROWN_OUT_RESET"
1064 | else:
1065 | reset_cause = "unknown"
1066 | #
1067 | if VGLOB['status'] == 8:
1068 | status = "idle"
1069 | elif VGLOB['status'] == 5:
1070 | status = "scanning"
1071 | elif VGLOB['status'] == 1:
1072 | status = "recovering"
1073 | else:
1074 | status = "working"
1075 | #
1076 | # MQTT addresses IN:\n""" + "\n".join( [ str(aaa) for aaa in VMQTT_SUB_LIST ] ) + """
1077 | vwebpage = """Directory listing on ESP. By writing /deldo?filename, files can be removed (dangerous).
1078 | Files with _old are safety copies after OTA, can be safely removed.
1079 | To disable webrepl, delete webrepl_cfg.py and reboot device.
1080 |
1081 | Dir: """ + str(os.listdir()) + """
1082 |
1083 | Global variables and settings: """ + str(VGLOB) + """
1084 |
1085 | Current time: """ + str(time.time()) + """
1086 |
1087 | Status: """ + status + """
1088 |
1089 | Error log:\n""" + "\n".join( ( str(aaa) for aaa in ERRORLOG ) ) + """
1090 |
1091 | Details:\n""" + "\n".join( ( str(aaa) for aaa in VWORK_LIST.items() ) ) + """
1092 |
1093 | Reset cause: """ + str(reset_cause) + """
1094 | Micropython version: """ + str(os.uname()) + """
1095 | Free RAM: """ + str(gc.mem_free()) + """."""
1096 | # v52_33 above, code opt
1097 | #vwebpage = vwebpage.encode('latin-1')
1098 | vwebpage = vwebpage.encode('ascii')
1099 | #
1100 | header = """HTTP/1.1 200 OK
1101 | Content-Type: text/plain
1102 | Content-Length: """ + str(len(vwebpage)) + """
1103 | Connection: close
1104 | """
1105 | #conn.sendall(header + "\r\n" + vwebpage)
1106 | await writer.awrite(header + "\r\n")
1107 | # INFO
1108 | # await writer.awrite(vwebpage)
1109 | # conn.close()
1110 | #####
1111 | #####
1112 | elif request == "/webrepl":
1113 | #requestval = requestfull.split('\r')[0].split(' ')[1].split('?')[1]
1114 | #vwebpage = str(requestval) + "\n" + str(os.listdir())
1115 | try:
1116 | fff = open('webrepl_cfg.py', 'w')
1117 | await fff.write("PASS = \'1234\'\n")
1118 | fff.close()
1119 | except Exception as e:
1120 | print2('fweb webrepl init issue ', e)
1121 | # try to open file, if fail no panic
1122 | #pass
1123 | header = """HTTP/1.1 302 Found
1124 | Content-Length: 0
1125 | Location: /reset
1126 | Connection: close
1127 | """
1128 | #conn.sendall(header + "\r\n" + vwebpage)
1129 | await writer.awrite(header + "\r\n")
1130 | # await writer.awrite(vwebpage)
1131 | # machine.reset()
1132 | #####
1133 | #####
1134 | elif request == "/scan":
1135 | #vwork['0'] = 'scan'
1136 | header = """HTTP/1.1 200 OK
1137 | Content-Type: text/html
1138 | Content-Length: 15
1139 | Connection: close
1140 |
1141 | Scan scheduled.
1142 | """
1143 | #conn.sendall(header + "\r\n" + vwebpage)
1144 | VWORK_LIST['00:00:00:00:00:00'][4].append( 'scan' )
1145 | await writer.awrite(header + "\r\n")
1146 | # await writer.awrite(vwebpage)
1147 | elif request == "/mqttauto":
1148 | # fpostpone()
1149 | fdisc(mqtth, VWORK_LIST, CONFIG2)
1150 | header = """HTTP/1.1 200 OK
1151 | Content-Type: text/html
1152 | Content-Length: 30
1153 | Connection: close
1154 |
1155 | MQTT Autodiscovery published.
1156 | """
1157 | #conn.sendall(header + "\r\n" + vwebpage)
1158 | await writer.awrite(header + "\r\n")
1159 | # await writer.awrite(vwebpage)
1160 | #####
1161 | #####
1162 | elif request == "/purge":
1163 | fclean(1)
1164 | header = """HTTP/1.1 200 OK
1165 | Content-Type: text/html
1166 | Content-Length: 77
1167 | Connection: close
1168 |
1169 | Old devices removed from the list and if necessary the work status was reset.
1170 | """
1171 | #conn.sendall(header + "\r\n" + vwebpage)
1172 | await writer.awrite(header + "\r\n")
1173 | # await writer.awrite(vwebpage)
1174 | #####
1175 | #####
1176 | elif request == "/ota":
1177 | # postpone job, to speed up ota
1178 | fpostpone()
1179 | # method="post"
1180 | vwebpage = """Usually upload main.py file. Sometimes boot.py file. Binary files do not work yet.
1181 |
1182 |
1186 | """
1187 | header = """HTTP/1.1 200 OK
1188 | Content-Type: text/html
1189 | Content-Length: """ + str(len(vwebpage)) + """
1190 | Connection: close
1191 | """
1192 | #conn.sendall(header + "\r\n" + vwebpage)
1193 | await writer.awrite(header + "\r\n")
1194 | # INFO
1195 | # await writer.awrite(vwebpage)
1196 | #####
1197 | #####
1198 | elif request == "/otado":
1199 | # postpone job, to speed up ota
1200 | fpostpone()
1201 | # stop scan if any
1202 | fstopscan(ble)
1203 | #
1204 | vwebpage = ''
1205 | #VGLOB = ''
1206 | #VSCAN_LIST = {}
1207 | #gc.collect()
1208 | # s.setblocking(0)
1209 | ### v52_09 automatic reset after upload
1210 | # Location: /reset
1211 | header = """HTTP/1.1 302 Found
1212 | Content-Length: 0
1213 | Location: /
1214 | Connection: close
1215 |
1216 | """
1217 | # =
1218 | #ble.active(False)
1219 | #gc.collect()
1220 | #headerin = conn.recv(500).decode()
1221 | headerin = yield from reader.read(500)
1222 | # print2(headerin)
1223 | headerin = headerin.decode()
1224 | boundaryin = headerin.split("boundary=", 2)[1].split('\r\n')[0]
1225 | lenin = int(headerin.split("\r\nContent-Length: ", 2)[1].split('\r\n')[0])
1226 | # dividing into 2000 bytes pieces
1227 | bufflen = round(lenin / float(str(round(lenin / 2500)) + ".5"))
1228 | #lenin = 0
1229 | #print2( "===" )
1230 | begin = 0
1231 | try:
1232 | os.remove('upload')
1233 | except Exception as e:
1234 | # try to upload file, if fail no panic
1235 | print2('fweb otado cleaning fail 1, this is fine', e)
1236 | #pass
1237 | fff = open('upload', 'wb')
1238 | while True:
1239 | #dataaa = conn.recv(bufflen).decode().split('\r\n--' + boundaryin, 2)
1240 | dataaa = yield from reader.read(bufflen)
1241 | dataaa = dataaa.decode().split('\r\n--' + boundaryin, 2)
1242 | splita = len(dataaa)
1243 | #print2( splita )
1244 | #filein += dataaa
1245 | if begin == 0 and splita == 3:
1246 | #print2( "= short" )
1247 | # short
1248 | # conn.sendall(header)
1249 | # conn.close()
1250 | await writer.awrite(header + "\r\n")
1251 | namein = dataaa[1].split(' filename="', 1)[1].split('"\r\n', 1)[0]
1252 | fff.write(dataaa[1].split('\r\n\r\n', 1)[1])
1253 | # done with success
1254 | begin = 3
1255 | break
1256 | if begin == 0 and splita == 2:
1257 | #print2( "= first" )
1258 | # first
1259 | namein = dataaa[1].split(' filename="', 1)[1].split('"\r\n', 1)[0]
1260 | fff.write(dataaa[1].split('\r\n\r\n', 1)[1])
1261 | begin = 1
1262 | elif begin == 1 and splita == 1:
1263 | #print2( "= middle" )
1264 | # middle
1265 | fff.write(dataaa[0])
1266 | elif begin == 1 and splita == 2:
1267 | #print2( "= last" )
1268 | # last
1269 | # conn.sendall(header)
1270 | await writer.awrite(header + "\r\n")
1271 | # conn.close()
1272 | fff.write(dataaa[0])
1273 | # done with success
1274 | begin = 3
1275 | break
1276 | fff.close()
1277 | # now replace new file
1278 | if begin == 3:
1279 | try:
1280 | os.remove(namein + "_old")
1281 | except Exception as e:
1282 | print2('fweb otado cleaning fail 2, this is fine', e)
1283 | #pass
1284 | try:
1285 | os.rename(namein, namein + "_old")
1286 | except Exception as e:
1287 | print2('fweb otado cleaning fail 3, this is fine', e)
1288 | os.rename('upload', namein)
1289 | #print2( "===" )
1290 | #print2( namein )
1291 | #print2( lenin )
1292 | dataaa = ''
1293 | ### v52_10
1294 | ### added wait time, so that header is nicely sent
1295 | await asyncio.sleep(0.3)
1296 | freset(time, machine, VWORK_LIST)
1297 | #ble.active(True)
1298 | #gc.collect()
1299 | #####
1300 | #####
1301 | elif request == "/reset":
1302 | fpostpone()
1303 | header = """HTTP/1.1 200 OK
1304 | Content-Type: text/html
1305 | Content-Length: 34
1306 | Connection: close
1307 |
1308 | Do reset ?
1309 | """
1310 | # Connection: close
1311 | # conn.sendall(header)
1312 | await writer.awrite(header + "\r\n")
1313 | # await writer.awrite(vwebpage)
1314 | # conn.close()
1315 | # time.sleep(2) # no sleep here ;)
1316 | #####
1317 | #####
1318 | elif request == "/resetdo":
1319 | header = """HTTP/1.1 302 Found
1320 | Content-Length: 0
1321 | Location: /
1322 | Connection: close
1323 |
1324 | """
1325 | # Connection: close
1326 | # conn.sendall(header)
1327 | await writer.awrite(header + "\r\n")
1328 | # await writer.awrite(vwebpage)
1329 | # conn.close()
1330 | # time.sleep(2) # no sleep here ;)
1331 | await asyncio.sleep(0.3) # was 0.3, 0.1 was not good
1332 | #machine.reset()
1333 | freset(time, machine, VWORK_LIST)
1334 | # time.sleep(1)
1335 | #####
1336 | #####
1337 | else:
1338 | # Server-Timing: text;dur=""" + str(time.ticks_ms() - timer2) + """, req;dur=""" + str(timer2 - timer1) + """
1339 | header = """HTTP/1.0 404 Not Found
1340 | Content-Type: text/plain
1341 | Content-Length: 23
1342 | Connection: close
1343 |
1344 | 404 No page like this.
1345 | """
1346 | # conn.sendall(header)
1347 | await writer.awrite(header + "\r\n")
1348 | # await writer.awrite(vwebpage)
1349 | # conn.close()
1350 | # END IF
1351 | # conn.close() # close or not ?
1352 | # whatever
1353 | try:
1354 | await writer.awrite(vwebpage)
1355 | #=await asyncio.sleep(0.1)
1356 | await writer.drain()
1357 | except Exception as e:
1358 | print2('fweb page flood 2a', e)
1359 | #vwebpage = b''
1360 | #resp = b'
1361 | return
1362 | # drain and sleep needed for good transfer
1363 | vwebpage = b''
1364 | resp = b''
1365 | # 0.3 is perfect, 0.4 makes delay issues, 0.2 breaks the connections sometimes
1366 | await asyncio.sleep(0.2)
1367 | # waiting until everything is sent, to close
1368 | try:
1369 | await reader.wait_closed()
1370 | except Exception as e:
1371 | print2('fweb page flood 2c', e)
1372 | # await reader.aclose()
1373 | #gc.collect()
1374 | #await asyncio.sleep(0.1)
1375 | #print2("-- f serving page done")
1376 | try:
1377 | # if run as thread, then stop thread
1378 | if not CONFIG2['loop']:
1379 | _thread.exit()
1380 | return
1381 | #pass
1382 | except Exception as e:
1383 | # if this fails, there is no reason to panic, function not in thread
1384 | print2('fweb loop_web close thread:', e)
1385 | # break
1386 | # catch OSError: [Errno 104] ECONNRESET ?
1387 | return
1388 |
1389 | #-####
1390 | #-####
1391 | #-#### check function, should be async as it causes locks
1392 |
1393 | def fcheck(var=None) -> None:
1394 | ###
1395 | ### check function run for cleaning, usually every 1-5 minutes
1396 | ###
1397 | ### var is needed, as this function is started in timer, which sends some arguments
1398 | ###
1399 | ### check of station/wifi, mqtt, ble, webpage server, ntptime, and watchdog
1400 | # do not stop scanning, but also do not return if scanning 50_11
1401 | # always collect here
1402 | gc.collect()
1403 | # check log
1404 | print2('fcheck start')
1405 | ### not needed here as already in fworker, v53_07
1406 | #try:
1407 | # wdt.feed()
1408 | #except Exception as e:
1409 | # print2('- fcheck wdt error, maybe not initialised ', e)
1410 | ###
1411 | ###
1412 | try:
1413 | mqtth.ping()
1414 | except Exception as e:
1415 | print2('fcheck mqtt ping error ', e)
1416 | #fmqtt_recover()
1417 | ###
1418 | global VGLOB
1419 | # do not stop if scanning 50_11
1420 | if VGLOB['status'] == 5:
1421 | print2('fcheck stopping scan')
1422 | # stop scan if any
1423 | fstopscan(ble)
1424 | #gc.collect()
1425 | #return
1426 | ### if all is fine, then get some globals first
1427 | global VSCAN_LIST
1428 | global VWORK_LIST
1429 | ftimenow = time.time()
1430 | ###
1431 | ###
1432 | # if still no work done - enforce reset
1433 | # should be slightly longer than router or mqtt server reboot, just in case
1434 | if ftimenow > ( VGLOB['timelast'] + ( VGLOB['delayquery'] * 6 ) ):
1435 | # 3 rounds no response, resetting
1436 | print2('fcheck reset recover')
1437 | ### here add job saving etc
1438 | freset(time, machine, VWORK_LIST)
1439 | #
1440 | # v52_35 readded the check, if no work is done in a few rounds
1441 | if ble.active() == False: #or ftimenow > ( VGLOB['timelast'] + (3 * VGLOB['delayquery'] ) ):
1442 | #or VGLOB['timescan'] > ( VGLOB['timelast'] + (2 * VGLOB['delayquery'] ) ):
1443 | print2('fcheck ble error')
1444 | #fpostpone(5)
1445 | ble.active(False) ### ADDED
1446 | time.sleep(0.1) ### ADDED, was 0.5
1447 | ble.active(True)
1448 | #gc.collect()
1449 | return
1450 | # fble_recover()
1451 | #
1452 | if station.isconnected() == False or station.ifconfig()[0] == '0.0.0.0':
1453 | print2('fcheck wifi error')
1454 | #fpostpone(5)
1455 | station.connect()
1456 | #gc.collect()
1457 | return
1458 | # machine.reset()
1459 | ###
1460 | ### this is in ms, most other calculations for time are in seconds
1461 | # v52_45 changed to 3 minutes, from 5
1462 | if time.ticks_ms() - mqtth.last_cpacket > 3 * 60 * 1000:
1463 | print2('fcheck bad mqtt')
1464 | # postpone is in the function recovery already, 50_7
1465 | #fpostpone(5)
1466 | # has to be in thread
1467 | _thread.start_new_thread(fmqtt_recover, ())
1468 | #fmqtt_recover()
1469 | #gc.collect()
1470 | return
1471 | ###
1472 | ### remove addresses older than x minutes, clean up old
1473 | ### v51_30, previously it was 20 minutes, now lowering to 10
1474 | ### v51_31, now lowering to 7 minutes
1475 | #last_contact = 9999
1476 | # this concerns VSCAN_LIST and not VWORK_LIST
1477 | for addr, val in VSCAN_LIST.items():
1478 | if ftimenow - val[3] > 7 * 60:
1479 | VSCAN_LIST.pop(addr)
1480 | ###
1481 | ### check ntp every 12 hours, v52_11 changed from 24h to 12h, with error catching
1482 | if ftimenow - VGLOB['timentp'] > 12 * 60 * 60:
1483 | VGLOB['timentp'] = ftimenow
1484 | fntp()
1485 | ###
1486 | ### check autodiscovery every 1 hours
1487 | if ftimenow - VGLOB['timedisc'] > 1 * 60 * 60:
1488 | VGLOB['timedisc'] = ftimenow
1489 | fdisc(mqtth, VWORK_LIST, CONFIG2)
1490 | ###
1491 | ### if some minutes from the last contact, then scan
1492 | # INFO: optimisations for scan scheduling make no sense, start every 1 minute
1493 | # here I use the global variable timescan
1494 | # still the VWORK_LIST['00:00:00:00:00:00'][0] system variable last work could be used
1495 | ###
1496 | ### v52_04 - TODO increase delay for OFF
1497 | if ftimenow - VGLOB['timework'] > VGLOB['delayquery']:
1498 | VGLOB['timework'] = ftimenow
1499 | for addr, val in VWORK_LIST.items():
1500 | # check if not connected [2] == None
1501 | # possible to check if no work [4] == None
1502 | #if ftimenow - val[1] > VGLOB['delayquery'] and VGLOB['timework']and val[2] == None and addr.replace(":", "")[0:6] == '001A22':
1503 | if val[2] == None and addr.replace(":", "")[0:6] == '001A22':
1504 | if len( VWORK_LIST[addr][4] ) == 0:
1505 | VWORK_LIST[addr][4].append( 'manual' )
1506 | #VWORK_LIST['00:00:00:00:00:00'][1] = time.time()
1507 | #if ftimenow - val[1] > VGLOB['delayquery'] and val[2] == None and addr.replace(":", "")[0:6] == '4C65A8':
1508 | if val[2] == None and addr.replace(":", "")[0:6] == '4C65A8':
1509 | if len( VWORK_LIST[addr][4] ) == 0:
1510 | VWORK_LIST[addr][4].append( 'gettemp' )
1511 | #VWORK_LIST['00:00:00:00:00:00'][1] = time.time()
1512 | ###
1513 | ###
1514 | ### if no work then start long scan
1515 | gc.collect()
1516 | #_thread.exit()
1517 | #VGLOB['status'] = 8
1518 | #print2('-- fcheck done')
1519 | return
1520 |
1521 | #-####
1522 | #-####
1523 | #-####
1524 |
1525 | def fmqtt_recover(var=None) -> None:
1526 | #gc.collect()
1527 | global VGLOB
1528 | print2('fmqtt recover')
1529 | #fpostpone(5) this is skipped here, but added while calling function
1530 | VGLOB['status'] = 1
1531 | # mqtth.keepalive = 130 # that is that ping fits easily 3 times
1532 | mqtth.keepalive = int(3.5 * VGLOB['delaycheck'])
1533 | #time.sleep(0.5)
1534 | mqtth.connect()
1535 | #time.sleep(0.1) # ???
1536 | mqtth.set_callback(fmqtt_irq)
1537 | #time.sleep(0.5)
1538 | for lll in VMQTT_SUB_LIST:
1539 | mqtth.subscribe(lll)
1540 | #time.sleep(0.1) # ???
1541 | # mqtth.subscribe(CONFIG2['mqtt_eq3_in'])
1542 | VGLOB['status'] = 8
1543 | #gc.collect()
1544 | try:
1545 | # if run as thread, then stop thread
1546 | _thread.exit()
1547 | #pass
1548 | except Exception as e:
1549 | # if this fails, there is no reason to panic, function not in thread
1550 | print2('fmqtt fblew close thread:', e)
1551 | ### v52_01 - cleaning
1552 | gc.collect()
1553 | return
1554 |
1555 | def fstart_server() -> None:
1556 | async_loop = asyncio.get_event_loop()
1557 | vserver = asyncio.start_server(loop_web, "0.0.0.0", 80, 2)
1558 | # loop=async_loop, limit=4096 # v53_10, none of these settings work
1559 | # backlog set to 2, this works
1560 | # limit streamreader - maybe 4096, as dividing OTA into 2500 bytes pieces
1561 | async_loop.create_task(vserver)
1562 | async_loop.run_forever()
1563 | return
1564 |
1565 | ################
1566 | ################ BASE
1567 | #gc.collect()
1568 | # -#### mqtt
1569 | # -#### this is moved from boot, to allow recovery
1570 | #_thread.stack_size(1024)
1571 | _thread.start_new_thread(fmqtt_recover, ())
1572 | # wait for connection at boot
1573 | # wait longer, was 2, but warning at boot
1574 | time.sleep(0.5) # shortened from 4 in 50_8
1575 |
1576 | #-####
1577 | #-####
1578 | # -#### connect interrupts
1579 | ble.irq(fble_irq)
1580 | time.sleep(0.5) # 2 in 50_8
1581 |
1582 | #-####
1583 | # -#### threads
1584 | #loopwebthread = _thread.start_new_thread(loop_web, ())
1585 |
1586 | #-####
1587 | #thread_web = _thread.start_new_thread(fstart_server, ())
1588 | _thread.start_new_thread(fstart_server, ())
1589 | time.sleep(0.5)
1590 |
1591 | #time.sleep(1)
1592 | #-####
1593 | fntp()
1594 | time.sleep(0.5)
1595 |
1596 | #-#### WDT
1597 | # do not move this to boot
1598 | # after boot is succesful
1599 | # changed from 3 to 2.5, v53_05
1600 | # changed from delaycheck (around 60 sec) to delaywork (around 2-3 sec), v53_07
1601 | # moved just before workers to avoid too long delays, but scanning does wdt so before scan
1602 | #wdt = machine.WDT( timeout = int( VGLOB['delaycheck'] * 3 ) * 1000 )
1603 | wdt = machine.WDT( timeout = int( VGLOB['delaywork'] * 4 ) * 1000 )
1604 | time.sleep(0.5)
1605 |
1606 | ### v52_03 usefull to start anyway in case delays in boot
1607 | fscan(0)
1608 | time.sleep(0.5)
1609 |
1610 | # -#### timers
1611 | # 4-5 seconds is completely fine for work
1612 | #
1613 | ### v51_30, added 0.5, so that it is never 0
1614 | timer_work.init( period = int( ( VGLOB['delaywork'] ) * 1000 ), callback=fworker)
1615 | # ### clean every x minutes, multiplied by 1000, as this is in ms
1616 | #_thread.start_new_thread(fble_write, (fworkout, worka[0], worka[1]))
1617 | time.sleep(0.5)
1618 | #
1619 | timer_check.init( period = int( VGLOB['delaycheck'] * 1000 ), callback=fcheck)
1620 |
1621 | time.sleep(0.5)
1622 | fdisc(mqtth, VWORK_LIST, CONFIG2)
1623 |
1624 | ### cleanup
1625 | gc.collect()
1626 |
1627 | print2("BOOTED")
1628 |
1629 | #-###
1630 |
--------------------------------------------------------------------------------
/code/main2.py:
--------------------------------------------------------------------------------
1 | #-####
2 | #-####
3 | #-####
4 |
5 | def ffind_handle(VWORK_LIST, handle: int) -> str:
6 | # get addr of the handle
7 | # cleaner 50_17
8 | try:
9 | return [kkk[0] for kkk in VWORK_LIST.items() if kkk[1][2]==handle][0]
10 | except:
11 | return '' # needed ?
12 |
13 | def ffind_status(VWORK_LIST, status: int = 7) -> str:
14 | # get addr of the connected state
15 | # cleaner 50_17
16 | try:
17 | return [kkk[0] for kkk in VWORK_LIST.items() if kkk[1][3]==status][0]
18 | except:
19 | return '' # needed ?
20 |
21 | def fdecode_addr(addr: bytes) -> str:
22 | # get str addr from hex mac
23 | result = []
24 | for iii in addr:
25 | result.append('{:0>2}'.format(str(hex(iii).split('x')[1])))
26 | return str((':').join(result)).upper()
27 |
28 |
29 | def fcode_addr(addr: str) -> bytes:
30 | # encrypt the str addr into connectable hex mac
31 | result = []
32 | for iii in addr.split(":"):
33 | result.append(int(str(iii), 16))
34 | return bytes(result)
35 |
36 | #### should be cleanly added here
37 |
38 | def faddwork() -> None:
39 | # function for adding work
40 | # should check where to add the work, and if this or similar work exists
41 | # if the work makes sense
42 | # in the future maybe this should be handled by the TRV object...
43 | pass
44 | return
45 |
46 | # v53_19 moved
47 | def freset(time, machine, VWORK_LIST):
48 | ### v52_08 reset function wrap
49 | ### TODO, add some functions like time save, job save, etc.
50 | #fff1 = open('temp.txt', 'w')
51 | #fff1.close()
52 | fff2 = open('temp.txt', 'w')
53 | fff2.write( str(VWORK_LIST) )
54 | fff2.close()
55 | #del fff
56 | time.sleep(0.2)
57 | #await fff.write("PASS = \'1234\'\n")
58 | machine.reset()
59 |
60 | # v53_19 moved
61 | def fdisconnect(ble, handle = 0) -> None:
62 | # disconnect and clean up, with all error catching etc
63 | try:
64 | ble.gap_disconnect(handle)
65 | except:
66 | # problem...
67 | return 1
68 | return
69 |
70 | # v53_19 moved
71 | def fstopscan(ble) -> None:
72 | try:
73 | ble.gap_scan(None)
74 | #sleep(0.1) # no sleep as it is used in time critical functions
75 | return
76 | except:
77 | pass
78 | return
79 |
80 |
81 | #-####
82 | #-####
83 | #-####
84 |
85 | def fdisc(mqtth, VWORK_LIST, CONFIG2) -> None:
86 | ###
87 | #print('= send mqtt autodiscovery')
88 | #gc.collect()
89 | #fpostpone()
90 | ### home assistant auto discovery
91 | ### discovery topic should be retained
92 | #print("publishing mqtt autodiscovery")
93 | ###
94 | ### loop through all trusted devices
95 | for hhh in VWORK_LIST:
96 | #time.sleep(0.1) # was 0.5
97 | #mac = str(hhh[9:17].replace(":", ""))
98 | mac = str(hhh.replace(":", "")[6:12])
99 | mac_type = str(hhh.replace(":", "")[0:6])
100 | ###
101 | if mac_type == '4C65A8' or mac_type == 'A4C138':
102 | #html_in += str(vvv) + "\n"
103 | ###
104 | #mac = str(hhh[9:17].replace(":", ""))
105 | ###
106 | topict = f'homeassistant/sensor/temp_{mac}_temp/config'
107 | devid = '"device": { "identifiers": [ "temp_' + mac + '_id" ], "name":"temp_' + mac + '_dev", "model":"JJ ESP32 Temp", "sw_version":"' + CONFIG2['__version__'] + '" }'
108 | devicet = '{"device_class": "temperature", "name": "Mijia ' + mac + ' Temperature", \
109 | "state_topic": "' + CONFIG2["mqtt_thermo"] + mac + '/radout/status", "unit_of_measurement": "°C", "value_template": "{{ value_json.temp }}", \
110 | "unique_id": "temp_' + mac + '_T", ' + devid + ' }'
111 | mqtth.publish(topict, bytes(devicet, 'ascii'), True)
112 | ###
113 | topich = f'homeassistant/sensor/temp_{mac}_hum/config'
114 | deviceh = '{"device_class": "humidity", "name": "Mijia ' + mac + ' Humidity", \
115 | "state_topic": "' + CONFIG2["mqtt_thermo"] + mac + '/radout/status", "unit_of_measurement": "%", "value_template": "{{ value_json.hum }}", \
116 | "unique_id": "temp_' + mac + '_H", '+ devid +' }'
117 | mqtth.publish(topich, bytes(deviceh, 'ascii'), True)
118 | ###
119 | # /hass/climate/climate3/radin/trv 00:1A:22:0E:C9:45 manual
120 | # /hass/climate/climate1/radout/status {"trv":"00:1A:22:0E:F6:F5","temp":"19.0","mode":"manual"}
121 | if mac_type == '001A22':
122 | ###
123 | # "~": "homeassistant/light/kitchen",
124 | # each eq3 has to have its own mqtt
125 | topicc = f'homeassistant/climate/clim_{mac}_eq3/config'
126 | if CONFIG2['mqtt_thermo_src'] != "":
127 | currtemp = '"curr_temp_t": "' + CONFIG2['mqtt_thermo_src'] + '/radout/status", "curr_temp_tpl": "{{value_json.temp}}", '
128 | else:
129 | currtemp = ""
130 | devid = '"device": { "identifiers": [ "clim_' + mac + '_id" ], "name": "clim_eq3_' + mac + '_dev", "model":"JJ ESP32 Clim EQ3", "sw_version":"' + CONFIG2['__version__'] + '" }'
131 | ### ver 51_29, min 4.5->OFF, 30.0->ON, recommended by wieluk-github
132 | devicec = '{ "name": "clim_eq3_' + mac + '", "unique_id": "clim_' + mac + '_thermostat_id", "modes": [ "heat" ], ' + devid + ', \
133 | "mode_cmd_t": "' + CONFIG2['mqtt_eq3'] + mac + '/radin/mode2", "mode_stat_t": "' + CONFIG2['mqtt_eq3'] + mac + '/radout/status", \
134 | "mode_stat_tpl": "{{value_json.mode2}}", "temp_cmd_t": "' + CONFIG2['mqtt_eq3'] + mac + '/radin/temp", \
135 | "temp_stat_t": "' + CONFIG2['mqtt_eq3'] + mac + '/radout/status", "temp_stat_tpl": "{{value_json.temp}}", \
136 | ' + currtemp + ' "min_temp": "4.5", "max_temp": "30.0", "temp_step": "0.5", \
137 | "hold_state_template": "{{value_json.mode}}", "hold_state_topic": "' + CONFIG2['mqtt_eq3'] + mac + '/radout/status", \
138 | "hold_command_topic": "' + CONFIG2['mqtt_eq3'] + mac + '/radin/mode", "hold_modes": [ "auto", "manual", "boost", "away" ] }'
139 | mqtth.publish(topicc, bytes(devicec, 'ascii'), True)
140 | ### battery and valve open sensors
141 | topicc = f'homeassistant/sensor/clim_{mac}_eq3_valve/config'
142 | devicec = '{ "name": "clim_eq3_' + mac + ' Valve", "unique_id": "clim_' + mac + '_valve_id" , ' + devid + ', \
143 | "state_topic": "' + CONFIG2['mqtt_eq3'] + mac + '/radout/status", "value_template": "{{ value_json.valve }}" }'
144 | mqtth.publish(topicc, bytes(devicec, 'ascii'), True)
145 | ###
146 | topicc = f'homeassistant/sensor/clim_{mac}_eq3_battery/config'
147 | devicec = '{ "name": "clim_eq3_' + mac + ' Battery", "unique_id": "clim_' + mac + '_battery_id", ' + devid + ', "device_class": "battery", \
148 | "state_topic": "' + CONFIG2['mqtt_eq3'] + mac + '/radout/status", "value_template": "{{ value_json.battery }}" }'
149 | mqtth.publish(topicc, bytes(devicec, 'ascii'), True)
150 | ###
151 | #del devicec
152 | # add True at the end to retain or retain=True
153 | if mac_type[0:4] == 'FFFF':
154 | #topicp = f'homeassistant/sensor/presence_{mac}_eq3/config'
155 | topicp = f'homeassistant/device_tracker/presence_{mac}_eq3/config'
156 | devid = '"device": { "identifiers": [ "tracker_' + mac + '_id" ], "name": "tracker_' + mac + '_dev", "model":"JJ ESP32 Tracker", "sw_version":"' + CONFIG2['__version__'] + '" }'
157 | devicep = '{"name": "Tracker ' + mac + '", "state_topic": "' + CONFIG2["mqtt_presence"] + mac + '/radout/status", ' + devid + ', \
158 | "expire_after": "240", "consider_home": "180", "unique_id": "tracker_' + mac + '", "source_type": "bluetooth" }'
159 | mqtth.publish(topicp, bytes(devicep, 'ascii'), True)
160 | #
161 | #del devicep
162 | ###
163 | ### here additional devices for publishing can be added
164 | ### v52_01 - cleaning
165 | #gc.collect()
166 | return
167 |
168 | #####
169 |
170 |
--------------------------------------------------------------------------------
/code/secret_cfg.py:
--------------------------------------------------------------------------------
1 | # -*- coding: latin-1 -*-
2 | #-### secret_cfg.py
3 |
4 | #-### variables / configuration
5 |
6 | #-###
7 | #-###
8 | #-### there variables are used only during boot, then, removed from memory
9 | CONFIG = {}
10 | CONFIG['freq'] = 240000000
11 | #CONFIG['freq'] = 160000000
12 | #-### pass is convoluted with [ ord(x) for x in list(pass) ]
13 | CONFIG['wifi_pass'] = [112, 97, 115, 115]
14 | CONFIG['wifi_name'] = 'wifiname'
15 | CONFIG['ntp_host'] = 'ntp_server'
16 | #
17 | CONFIG2 = {}
18 | CONFIG2['mqtt_eq3_in'] = '/hass/climate/climate/radin/trv'
19 | CONFIG2['mqtt_eq3_out'] = '/hass/climate/climate/radout/status'
20 | CONFIG2['mqtt_mijia_out'] = '/hass/climate/thermo/status'
21 |
22 | CONFIG2['mqtt_eq3'] = 'esp/climate/'
23 | CONFIG2['mqtt_thermo'] = 'esp/thermo/'
24 | # source of real temperature, some other mqtt thermometer or something
25 | CONFIG2['mqtt_thermo_src'] = ''
26 | CONFIG2['mqtt_presence'] = 'esp/presence/'
27 |
28 | CONFIG2['mqtt_srv'] = 'mqtt_srv'
29 | CONFIG2['mqtt_usr'] = 'mqtt_usr'
30 | CONFIG2['mqtt_pass'] = 'mqtt_pass'
31 |
32 | #-### if keep loop is set to 0, then loops die
33 | #-### if you want to kill loops/threads, set this to 0
34 | #-### good for debugging and responsiver webrepl
35 | CONFIG2['loop'] = 1
36 |
37 | #-### done here
--------------------------------------------------------------------------------
/code/simple2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: latin-1 -*-
2 | import usocket as socket
3 | import uselect
4 | from utime import ticks_add,ticks_ms,ticks_diff
5 | class MQTTException(Exception):0
6 | def pid_gen(pid=0):
7 | A=pid
8 | while True:A=A+1 if A<65535 else 1;yield A
9 | class MQTTClient:
10 | def __init__(A,client_id,server,port=0,user=None,password=None,keepalive=0,ssl=False,ssl_params=None,socket_timeout=5,message_timeout=10):
11 | C=ssl_params;B=port
12 | if B==0:B=8883 if ssl else 1883
13 | A.client_id=client_id;A.sock=None;A.poller=None;A.server=server;A.port=B;A.ssl=ssl;A.ssl_params=C if C else{};A.newpid=pid_gen()
14 | if not getattr(A,'cb',None):A.cb=None
15 | if not getattr(A,'cbstat',None):A.cbstat=lambda p,s:None
16 | A.user=user;A.pswd=password;A.keepalive=keepalive;A.lw_topic=None;A.lw_msg=None;A.lw_qos=0;A.lw_retain=False;A.rcv_pids={};A.last_ping=ticks_ms();A.last_cpacket=ticks_ms();A.socket_timeout=socket_timeout;A.message_timeout=message_timeout
17 | def _read(A,n):
18 | try:
19 | B=b''
20 | for C in range(n):A._sock_timeout(A.poller_r,A.socket_timeout);B+=A.sock.read(1)
21 | except AttributeError:raise MQTTException(8)
22 | if B==b'':raise MQTTException(1)
23 | if len(B)!=n:raise MQTTException(2)
24 | return B
25 | def _write(A,bytes_wr,length=-1):
26 | D=bytes_wr;B=length
27 | try:A._sock_timeout(A.poller_w,A.socket_timeout);C=A.sock.write(D,B)
28 | except AttributeError:raise MQTTException(8)
29 | if B<0:
30 | if C!=len(D):raise MQTTException(3)
31 | elif C!=B:raise MQTTException(3)
32 | return C
33 | def _send_str(A,s):assert len(s)<65536;A._write(len(s).to_bytes(2,'big'));A._write(s)
34 | def _recv_len(D):
35 | A=0;B=0
36 | while 1:
37 | C=D._read(1)[0];A|=(C&127)<127:buf[B]=A&127|128;A>>=7;B+=1
43 | buf[B]=A;return B+1
44 | def _sock_timeout(B,poller,socket_timeout):
45 | A=socket_timeout
46 | if B.sock:
47 | C=poller.poll(-1 if A is None else int(A*1000))
48 | if not C:raise MQTTException(30)
49 | else:raise MQTTException(28)
50 | def set_callback(A,f):A.cb=f
51 | def set_callback_status(A,f):A.cbstat=f
52 | def set_last_will(A,topic,msg,retain=False,qos=0):B=topic;assert 0<=qos<=2;assert B;A.lw_topic=B;A.lw_msg=msg;A.lw_qos=qos;A.lw_retain=retain
53 | def connect(A,clean_session=True):
54 | E=clean_session;A.sock=socket.socket();A.poller_r=uselect.poll();A.poller_r.register(A.sock,uselect.POLLIN);A.poller_w=uselect.poll();A.poller_w.register(A.sock,uselect.POLLOUT);G=socket.getaddrinfo(A.server,A.port)[0][-1];A.sock.connect(G)
55 | if A.ssl:import ussl;A.sock=ussl.wrap_socket(A.sock,**A.ssl_params)
56 | F=bytearray(b'\x10\x00\x00\x00\x00\x00');B=bytearray(b'\x00\x04MQTT\x04\x00\x00\x00');D=10+2+len(A.client_id);B[7]=bool(E)<<1
57 | if bool(E):A.rcv_pids.clear()
58 | if A.user is not None:
59 | D+=2+len(A.user);B[7]|=1<<7
60 | if A.pswd is not None:D+=2+len(A.pswd);B[7]|=1<<6
61 | if A.keepalive:assert A.keepalive<65536;B[8]|=A.keepalive>>8;B[9]|=A.keepalive&255
62 | if A.lw_topic:D+=2+len(A.lw_topic)+2+len(A.lw_msg);B[7]|=4|(A.lw_qos&1)<<3|(A.lw_qos&2)<<3;B[7]|=A.lw_retain<<5
63 | H=A._varlen_encode(D,F,1);A._write(F,H);A._write(B);A._send_str(A.client_id)
64 | if A.lw_topic:A._send_str(A.lw_topic);A._send_str(A.lw_msg)
65 | if A.user is not None:
66 | A._send_str(A.user)
67 | if A.pswd is not None:A._send_str(A.pswd)
68 | C=A._read(4)
69 | if not(C[0]==32 and C[1]==2):raise MQTTException(29)
70 | if C[3]!=0:
71 | if 1<=C[3]<=5:raise MQTTException(20+C[3])
72 | else:raise MQTTException(20,C[3])
73 | A.last_cpacket=ticks_ms();return C[2]&1
74 | def disconnect(A):A._write(b'\xe0\x00');A.poller_r.unregister(A.sock);A.poller_w.unregister(A.sock);A.sock.close();A.sock=None;A.poller=None
75 | def ping(A):A._write(b'\xc0\x00');A.last_ping=ticks_ms()
76 | def publish(A,topic,msg,retain=False,qos=0,dup=False):
77 | E=topic;B=qos;assert B in(0,1);C=bytearray(b'0\x00\x00\x00\x00');C[0]|=B<<1|retain|int(dup)<<3;F=2+len(E)+len(msg)
78 | if B>0:F+=2
79 | G=A._varlen_encode(F,C,1);A._write(C,G);A._send_str(E)
80 | if B>0:D=next(A.newpid);A._write(D.to_bytes(2,'big'))
81 | A._write(msg)
82 | if B>0:A.rcv_pids[D]=ticks_add(ticks_ms(),A.message_timeout*1000);return D
83 | def subscribe(A,topic,qos=0):E=topic;assert qos in(0,1);assert A.cb is not None,'Subscribe callback is not set';B=bytearray(b'\x82\x00\x00\x00\x00\x00\x00');C=next(A.newpid);F=2+2+len(E)+1;D=A._varlen_encode(F,B,1);B[D:D+2]=C.to_bytes(2,'big');A._write(B,D+2);A._send_str(E);A._write(qos.to_bytes(1,'little'));A.rcv_pids[C]=ticks_add(ticks_ms(),A.message_timeout*1000);return C
84 | def _message_timeout(A):
85 | C=ticks_ms()
86 | for (B,D) in A.rcv_pids.items():
87 | if ticks_diff(D,C)<=0:A.rcv_pids.pop(B);A.cbstat(B,0)
88 | def check_msg(A):
89 | if A.sock:
90 | if not A.poller_r.poll(-1 if A.socket_timeout is None else 1):A._message_timeout();return None
91 | try:
92 | G=A._read(1)
93 | if not G:A._message_timeout();return None
94 | except OSError as H:
95 | if H.args[0]==110:A._message_timeout();return None
96 | else:raise H
97 | else:raise MQTTException(28)
98 | if G==b'\xd0':
99 | if A._read(1)[0]!=0:MQTTException(-1)
100 | A.last_cpacket=ticks_ms();return
101 | B=G[0]
102 | if B==64:
103 | D=A._read(1)
104 | if D!=b'\x02':raise MQTTException(-1)
105 | F=int.from_bytes(A._read(2),'big')
106 | if F in A.rcv_pids:A.last_cpacket=ticks_ms();A.rcv_pids.pop(F);A.cbstat(F,1)
107 | else:A.cbstat(F,2)
108 | if B==144:
109 | C=A._read(4)
110 | if C[0]!=3:raise MQTTException(40,C)
111 | if C[3]==128:raise MQTTException(44)
112 | if C[3]not in(0,1,2):raise MQTTException(40,C)
113 | E=C[2]|C[1]<<8
114 | if E in A.rcv_pids:A.last_cpacket=ticks_ms();A.rcv_pids.pop(E);A.cbstat(E,1)
115 | else:raise MQTTException(5)
116 | A._message_timeout()
117 | if B&240!=48:return B
118 | D=A._recv_len();I=int.from_bytes(A._read(2),'big');J=A._read(I);D-=I+2
119 | if B&6:E=int.from_bytes(A._read(2),'big');D-=2
120 | K=A._read(D)if D else b'';L=B&1;M=B&8;A.cb(J,K,bool(L),bool(M));A.last_cpacket=ticks_ms()
121 | if B&6==2:A._write(b'@\x02');A._write(E.to_bytes(2,'big'))
122 | elif B&6==4:raise NotImplementedError()
123 | elif B&6==6:raise MQTTException(-1)
124 | def wait_msg(A):B=A.socket_timeout;A.socket_timeout=None;C=A.check_msg();A.socket_timeout=B;return C
--------------------------------------------------------------------------------