├── DS3231 ├── README.md ├── ds3231_gen.py ├── ds3231_gen_test.py ├── ds3231_pb.py ├── ds3231_port.py ├── ds3231_port_test.py └── ds3231_test.py ├── ESP32 └── ESP32-Devkit-C-pinout.pdf ├── ESP8266 ├── benchmark.py └── conn.py ├── LICENSE ├── PICOWEB.md ├── PicoWeb ├── picoweb │ ├── __init__.py │ ├── example_webapp.py │ ├── example_webapp2.py │ ├── templates │ │ └── squares.tpl │ └── utils.py ├── pkg_resources.py ├── uasyncio │ ├── __init__.py │ └── core.py ├── ulogging.py └── utemplate │ ├── compiled.py │ └── source.py ├── QUATERNIONS.md ├── README.md ├── RSHELL_MACROS.md ├── SERIALISATION.md ├── astronomy ├── CDR_license.txt ├── README.md ├── lunartick.jpg ├── moonphase.py ├── package.json ├── sun_moon.py └── sun_moon_test.py ├── bitmap └── bitmap.py ├── buildcheck └── buildcheck.py ├── data_to_py └── data_to_py.py ├── date ├── DATE.md └── date.py ├── encoders ├── ENCODERS.md ├── encoder.py ├── encoder_conditioner.png ├── encoder_conditioner_digital.png ├── encoder_portable.py ├── encoder_rp2.py ├── encoder_timed.py ├── quadrature.jpg ├── state_diagram.png └── synchroniser.png ├── fastbuild ├── 47-ftdi.rules ├── 48-wipy.rules ├── 49-micropython.rules ├── README.md ├── buildesp ├── buildnew ├── buildpyb ├── pyb_boot └── pyb_check ├── functor_singleton ├── README.md └── examples.py ├── goertzel ├── README.md └── goertzel3.py ├── import └── IMPORT.md ├── micropip ├── README.md └── micropip.py ├── mutex ├── README.md ├── mutex.py └── mutex_test.py ├── ntptime └── ntptime.py ├── parse2d ├── README.md ├── demo_parse2d.py └── parse2d.py ├── phase └── README.md ├── power ├── README.md ├── SignalConditioner.fzz ├── images │ ├── IMAGES.md │ ├── correctrange.JPG │ ├── integrate.JPG │ ├── interior.JPG │ ├── microwave.JPG │ ├── outside.JPG │ ├── plot.JPG │ └── underrange.JPG ├── mains.py └── mt.py ├── pyboard_d └── README.md ├── quaternion ├── graph3d.py ├── quat.py ├── quat_test.py ├── setup3d.py ├── setup3d_lcd160cr.py ├── test3d.py └── test_imu.py ├── random ├── cheap_rand.py ├── random.py └── yasmarang.py ├── resilient ├── README.md ├── application.py ├── client_id.py ├── client_w.py ├── primitives.py └── server.py ├── reverse └── reverse.py ├── sequence └── check_mid.py ├── soft_wdt ├── soft_wdt.py └── swdt_tests.py ├── temp └── read_2_hard.jpg ├── timed_function ├── timed_func.py └── timeout.py └── watchdog └── wdog.py /DS3231/ds3231_gen.py: -------------------------------------------------------------------------------- 1 | # ds3231_gen.py General purpose driver for DS3231 precison real time clock. 2 | 3 | # Author: Peter Hinch 4 | # Copyright Peter Hinch 2023 Released under the MIT license. 5 | 6 | # Rewritten from datasheet to support alarms. Sources studied: 7 | # WiPy driver at https://github.com/scudderfish/uDS3231 8 | # https://github.com/notUnique/DS3231micro 9 | 10 | # Assumes date > Y2K and 24 hour clock. 11 | 12 | import time 13 | import machine 14 | 15 | 16 | _ADDR = const(104) 17 | 18 | EVERY_SECOND = 0x0F # Exported flags 19 | EVERY_MINUTE = 0x0E 20 | EVERY_HOUR = 0x0C 21 | EVERY_DAY = 0x80 22 | EVERY_WEEK = 0x40 23 | EVERY_MONTH = 0 24 | 25 | try: 26 | rtc = machine.RTC() 27 | except: 28 | print("Warning: machine module does not support the RTC.") 29 | rtc = None 30 | 31 | 32 | class Alarm: 33 | def __init__(self, device, n): 34 | self._device = device 35 | self._i2c = device.ds3231 36 | self.alno = n # Alarm no. 37 | self.offs = 7 if self.alno == 1 else 0x0B # Offset into address map 38 | self.mask = 0 39 | 40 | def _reg(self, offs : int, buf = bytearray(1)) -> int: # Read a register 41 | self._i2c.readfrom_mem_into(_ADDR, offs, buf) 42 | return buf[0] 43 | 44 | def enable(self, run): 45 | flags = self._reg(0x0E) | 4 # Disable square wave 46 | flags = (flags | self.alno) if run else (flags & ~self.alno & 0xFF) 47 | self._i2c.writeto_mem(_ADDR, 0x0E, flags.to_bytes(1, "little")) 48 | 49 | def __call__(self): # Return True if alarm is set 50 | return bool(self._reg(0x0F) & self.alno) 51 | 52 | def clear(self): 53 | flags = (self._reg(0x0F) & ~self.alno) & 0xFF 54 | self._i2c.writeto_mem(_ADDR, 0x0F, flags.to_bytes(1, "little")) 55 | 56 | def set(self, when, day=0, hr=0, min=0, sec=0): 57 | if when not in (0x0F, 0x0E, 0x0C, 0x80, 0x40, 0): 58 | raise ValueError("Invalid alarm specifier.") 59 | self.mask = when 60 | if when == EVERY_WEEK: 61 | day += 1 # Setting a day of week 62 | self._device.set_time((0, 0, day, hr, min, sec, 0, 0), self) 63 | self.enable(True) 64 | 65 | 66 | class DS3231: 67 | def __init__(self, i2c): 68 | self.ds3231 = i2c 69 | self.alarm1 = Alarm(self, 1) 70 | self.alarm2 = Alarm(self, 2) 71 | if _ADDR not in self.ds3231.scan(): 72 | raise RuntimeError(f"DS3231 not found on I2C bus at {_ADDR}") 73 | 74 | def get_time(self, data=bytearray(7)): 75 | def bcd2dec(bcd): # Strip MSB 76 | return ((bcd & 0x70) >> 4) * 10 + (bcd & 0x0F) 77 | 78 | self.ds3231.readfrom_mem_into(_ADDR, 0, data) 79 | ss, mm, hh, wday, DD, MM, YY = [bcd2dec(x) for x in data] 80 | YY += 2000 81 | # Time from DS3231 in time.localtime() format (less yday) 82 | result = YY, MM, DD, hh, mm, ss, wday - 1, 0 83 | return result 84 | 85 | # Output time or alarm data to device 86 | # args: tt A datetime tuple. If absent uses localtime. 87 | # alarm: An Alarm instance or None if setting time 88 | def set_time(self, tt=None, alarm=None): 89 | # Given BCD value return a binary byte. Modifier: 90 | # Set MSB if any of bit(1..4) or bit 7 set, set b6 if mod[6] 91 | def gbyte(dec, mod=0): 92 | tens, units = divmod(dec, 10) 93 | n = (tens << 4) + units 94 | n |= 0x80 if mod & 0x0F else mod & 0xC0 95 | return n.to_bytes(1, "little") 96 | 97 | YY, MM, mday, hh, mm, ss, wday, yday = time.localtime() if tt is None else tt 98 | mask = 0 if alarm is None else alarm.mask 99 | offs = 0 if alarm is None else alarm.offs 100 | if alarm is None or alarm.alno == 1: # Has a seconds register 101 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(ss, mask & 1)) 102 | offs += 1 103 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(mm, mask & 2)) 104 | offs += 1 105 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(hh, mask & 4)) # Sets to 24hr mode 106 | offs += 1 107 | if alarm is not None: # Setting an alarm - mask holds MS 2 bits 108 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(mday, mask)) 109 | else: # Setting time 110 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(wday + 1)) # 1 == Monday, 7 == Sunday 111 | offs += 1 112 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(mday)) # Day of month 113 | offs += 1 114 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(MM, 0x80)) # Century bit (>Y2K) 115 | offs += 1 116 | self.ds3231.writeto_mem(_ADDR, offs, gbyte(YY - 2000)) 117 | 118 | def temperature(self): 119 | def twos_complement(input_value: int, num_bits: int) -> int: 120 | mask = 2 ** (num_bits - 1) 121 | return -(input_value & mask) + (input_value & ~mask) 122 | 123 | t = self.ds3231.readfrom_mem(_ADDR, 0x11, 2) 124 | i = t[0] << 8 | t[1] 125 | return twos_complement(i >> 6, 10) * 0.25 126 | 127 | def __str__(self, buf=bytearray(0x13)): # Debug dump of device registers 128 | self.ds3231.readfrom_mem_into(_ADDR, 0, buf) 129 | s = "" 130 | for n, v in enumerate(buf): 131 | s = f"{s}0x{n:02x} 0x{v:02x} {v >> 4:04b} {v & 0xF :04b}\n" 132 | if not (n + 1) % 4: 133 | s = f"{s}\n" 134 | return s 135 | -------------------------------------------------------------------------------- /DS3231/ds3231_gen_test.py: -------------------------------------------------------------------------------- 1 | # ds3231_gen_test.py Test script for ds3231_gen.oy. 2 | 3 | # Author: Peter Hinch 4 | # Copyright Peter Hinch 2023 Released under the MIT license. 5 | 6 | from machine import SoftI2C, Pin 7 | from ds3231_gen import * 8 | import time 9 | import uasyncio as asyncio 10 | 11 | def dt_tuple(dt): 12 | return time.localtime(time.mktime(dt)) # Populate weekday field 13 | 14 | i2c = SoftI2C(scl=Pin(16, Pin.OPEN_DRAIN, value=1), sda=Pin(17, Pin.OPEN_DRAIN, value=1)) 15 | d = DS3231(i2c) 16 | 17 | async def wait_for_alarm(alarm, t, target): # Wait for n seconds for an alarm, check time of occurrence 18 | print(f"Wait {t} secs for alarm...") 19 | if alarm.alno == 2: 20 | target = 0 # Alarm 2 does not support secs 21 | while t: 22 | if alarm(): 23 | return target - 1 <= d.get_time()[5] <= target + 1 24 | await asyncio.sleep(1) 25 | t -= 1 26 | return False 27 | 28 | async def test_alarm(alarm): 29 | print("Test weekly alarm") 30 | result = True 31 | dt = dt_tuple((2023, 2, 28, 23, 59, 50, 0, 0)) 32 | d.set_time(dt) # day is 1 33 | alarm.set(EVERY_WEEK, day=2, sec=5) # Weekday 34 | alarm.clear() 35 | if await wait_for_alarm(alarm, 20, 5): # Should alarm on rollover from day 1 to 2 36 | print("\x1b[32mWeek test 1 pass\x1b[39m") 37 | else: 38 | print("\x1b[91mWeek test 1 fail\x1b[39m") 39 | result = False 40 | 41 | dt = dt_tuple((2023, 2, 27, 23, 59, 50, 0, 0)) 42 | d.set_time(dt) # day is 0 43 | alarm.set(EVERY_WEEK, day=2, sec=5) 44 | alarm.clear() 45 | if await wait_for_alarm(alarm, 20, 5): # Should not alarm on rollover from day 0 to 1 46 | print("\x1b[91mWeek test 2 fail\x1b[39m") 47 | result = False 48 | else: 49 | print("\x1b[32mWeek test 2 pass\x1b[39m") 50 | 51 | print("Test monthly alarm") 52 | dt = dt_tuple((2023, 2, 28, 23, 59, 50, 0, 0)) 53 | d.set_time(dt) # day is 1 54 | alarm.set(EVERY_MONTH, day=1, sec=5) # Day of month 55 | alarm.clear() 56 | if await wait_for_alarm(alarm, 20, 5): # Should alarm on rollover from 28th to 1st 57 | print("\x1b[32mMonth test 1 pass\x1b[39m") 58 | else: 59 | print("\x1b[91mMonth test 1 fail\x1b[39m") 60 | result = False 61 | 62 | dt = dt_tuple((2023, 2, 27, 23, 59, 50, 0, 0)) 63 | d.set_time(dt) # day is 0 64 | alarm.set(EVERY_MONTH, day=1, sec=5) 65 | alarm.clear() 66 | if await wait_for_alarm(alarm, 20, 5): # Should not alarm on rollover from day 27 to 28 67 | print("\x1b[91mMonth test 2 fail\x1b[39m") 68 | result = False 69 | else: 70 | print("\x1b[32mMonth test 2 pass\x1b[39m") 71 | 72 | print("Test daily alarm") 73 | dt = dt_tuple((2023, 2, 1, 23, 59, 50, 0, 0)) 74 | d.set_time(dt) # 23:59:50 75 | alarm.set(EVERY_DAY, hr=0, sec=5) 76 | alarm.clear() 77 | if await wait_for_alarm(alarm, 20, 5): # Should alarm at 00:00:05 78 | print("\x1b[32mDaily test 1 pass\x1b[39m") 79 | else: 80 | print("\x1b[91mDaily test 1 fail\x1b[39m") 81 | result = False 82 | 83 | dt = dt_tuple((2023, 2, 1, 22, 59, 50, 0, 0)) 84 | d.set_time(dt) # 22:59:50 85 | alarm.set(EVERY_DAY, hr=0, sec=5) 86 | alarm.clear() 87 | if await wait_for_alarm(alarm, 20, 5): # Should not alarm at 22:00:05 88 | print("\x1b[91mDaily test 2 fail\x1b[39m") 89 | result = False 90 | else: 91 | print("\x1b[32mDaily test 2 pass\x1b[39m") 92 | 93 | print("Test hourly alarm") 94 | dt = dt_tuple((2023, 2, 1, 20, 9, 50, 0, 0)) 95 | d.set_time(dt) # 20:09:50 96 | alarm.set(EVERY_HOUR, min=10, sec=5) 97 | alarm.clear() 98 | if await wait_for_alarm(alarm, 20, 5): # Should alarm at xx:10:05 99 | print("\x1b[32mDaily test 1 pass\x1b[39m") 100 | else: 101 | print("\x1b[91mDaily test 1 fail\x1b[39m") 102 | result = False 103 | 104 | dt = dt_tuple((2023, 2, 1, 20, 29, 50, 0, 0)) 105 | d.set_time(dt) # 20:29:50 106 | alarm.set(EVERY_HOUR, min=10, sec=5) 107 | alarm.clear() 108 | if await wait_for_alarm(alarm, 20, 5): # Should not alarm at xx:30:05 109 | print("\x1b[91mDaily test 2 fail\x1b[39m") 110 | result = False 111 | else: 112 | print("\x1b[32mDaily test 2 pass\x1b[39m") 113 | 114 | print("Test minute alarm") 115 | dt = dt_tuple((2023, 2, 1, 20, 9, 50, 0, 0)) 116 | d.set_time(dt) # 20:09:50 117 | alarm.set(EVERY_MINUTE, sec=5) 118 | alarm.clear() 119 | if await wait_for_alarm(alarm, 20, 5): # Should alarm at xx:xx:05 120 | print("\x1b[32mMinute test 1 pass\x1b[39m") 121 | else: 122 | print("\x1b[91mMinute test 1 fail\x1b[39m") 123 | result = False 124 | 125 | if alarm.alno == 2: 126 | print("Skipping minute test 2: requires seconds resolution unsupported by alarm2.") 127 | else: 128 | dt = dt_tuple((2023, 2, 1, 20, 29, 50, 0, 0)) 129 | d.set_time(dt) # 20:29:50 130 | alarm.set(EVERY_MINUTE, sec=30) 131 | alarm.clear() 132 | if await wait_for_alarm(alarm, 20, 5): # Should not alarm at xx:xx:05 133 | print("\x1b[91mMinute test 2 fail\x1b[39m") 134 | result = False 135 | else: 136 | print("\x1b[32mMinute test 2 pass\x1b[39m") 137 | 138 | if alarm.alno == 2: 139 | print("Skipping seconds test: unsupported by alarm2.") 140 | else: 141 | print("Test seconds alarm (test takes 1 minute)") 142 | dt = dt_tuple((2023, 2, 1, 20, 9, 20, 0, 0)) 143 | d.set_time(dt) # 20:09:20 144 | alarm.set(EVERY_SECOND) 145 | alarm_count = 0 146 | t = time.ticks_ms() 147 | while time.ticks_diff(time.ticks_ms(), t) < 60_000: 148 | alarm.clear() 149 | while not d.alarm1(): 150 | await asyncio.sleep(0) 151 | alarm_count += 1 152 | if 59 <= alarm_count <= 61: 153 | print("\x1b[32mSeconds test 1 pass\x1b[39m") 154 | else: 155 | print("\x1b[91mSeconds test 2 fail\x1b[39m") 156 | result = False 157 | alarm.enable(False) 158 | return result 159 | 160 | 161 | async def main(): 162 | print("Testing alarm 1") 163 | result = await test_alarm(d.alarm1) 164 | print("Teting alarm 2") 165 | result |= await test_alarm(d.alarm2) 166 | if result: 167 | print("\x1b[32mAll tests passed\x1b[39m") 168 | else: 169 | print("\x1b[91mSome tests failed\x1b[39m") 170 | 171 | asyncio.run(main()) 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /DS3231/ds3231_pb.py: -------------------------------------------------------------------------------- 1 | # Pyboard driver for DS3231 precison real time clock. 2 | # Adapted from WiPy driver at https://github.com/scudderfish/uDS3231 3 | # Includes routine to calibrate the Pyboard's RTC from the DS3231 4 | # precison of calibration further improved by timing Pyboard RTC transition 5 | # Adapted by Peter Hinch, Jan 2016, Jan 2020 for Pyboard D 6 | 7 | # Pyboard D rtc.datetime()[7] counts microseconds. See end of page on 8 | # https://pybd.io/hw/pybd_sfxw.htm 9 | # Note docs for machine.RTC are wrong for Pyboard. The undocumented datetime 10 | # method seems to be the only way to set the RTC and it follows the same 11 | # convention as the pyb RTC's datetime method. 12 | 13 | import utime 14 | import machine 15 | import os 16 | 17 | d_series = os.uname().machine.split(' ')[0][:4] == 'PYBD' 18 | if d_series: 19 | machine.Pin.board.EN_3V3.value(1) 20 | 21 | DS3231_I2C_ADDR = 104 22 | 23 | class DS3231Exception(OSError): 24 | pass 25 | 26 | rtc = machine.RTC() 27 | 28 | def get_ms(s): # Pyboard 1.x datetime to ms. Caller handles rollover. 29 | return (1000 * (255 - s[7]) >> 8) + s[6] * 1000 + s[5] * 60_000 + s[4] * 3_600_000 30 | 31 | def get_us(s): # For Pyboard D: convert datetime to μs. Caller handles rollover 32 | return s[7] + s[6] * 1_000_000 + s[5] * 60_000_000 + s[4] * 3_600_000_000 33 | 34 | def bcd2dec(bcd): 35 | return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f)) 36 | 37 | def dec2bcd(dec): 38 | tens, units = divmod(dec, 10) 39 | return (tens << 4) + units 40 | 41 | def tobytes(num): 42 | return num.to_bytes(1, 'little') 43 | 44 | class DS3231: 45 | def __init__(self, i2c): 46 | self.ds3231 = i2c 47 | self.timebuf = bytearray(7) 48 | if DS3231_I2C_ADDR not in self.ds3231.scan(): 49 | raise DS3231Exception("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR) 50 | 51 | def get_time(self, set_rtc=False): 52 | if set_rtc: 53 | self.await_transition() # For accuracy set RTC immediately after a seconds transition 54 | else: 55 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) # don't wait 56 | return self.convert(set_rtc) 57 | 58 | def convert(self, set_rtc=False): 59 | data = self.timebuf 60 | ss = bcd2dec(data[0]) 61 | mm = bcd2dec(data[1]) 62 | if data[2] & 0x40: 63 | hh = bcd2dec(data[2] & 0x1f) 64 | if data[2] & 0x20: 65 | hh += 12 66 | else: 67 | hh = bcd2dec(data[2]) 68 | wday = data[3] 69 | DD = bcd2dec(data[4]) 70 | MM = bcd2dec(data[5] & 0x1f) 71 | YY = bcd2dec(data[6]) 72 | if data[5] & 0x80: 73 | YY += 2000 74 | else: 75 | YY += 1900 76 | if set_rtc: 77 | rtc.datetime((YY, MM, DD, wday, hh, mm, ss, 0)) 78 | return (YY, MM, DD, hh, mm, ss, wday -1, 0) # Time from DS3231 in time.time() format (less yday) 79 | 80 | def save_time(self): 81 | (YY, MM, mday, hh, mm, ss, wday, yday) = utime.localtime() # Based on RTC 82 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss))) 83 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm))) 84 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode 85 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday 86 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month 87 | if YY >= 2000: 88 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM) | 0b10000000)) # Century bit 89 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-2000))) 90 | else: 91 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM))) 92 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900))) 93 | 94 | def await_transition(self): # Wait until DS3231 seconds value changes 95 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 96 | ss = self.timebuf[0] 97 | while ss == self.timebuf[0]: 98 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 99 | return self.timebuf 100 | 101 | # Get calibration factor for Pyboard RTC. Note that the DS3231 doesn't have millisecond resolution so we 102 | # wait for a seconds transition to emulate it. 103 | # This function returns the required calibration factor for the RTC (approximately the no. of ppm the 104 | # RTC lags the DS3231). 105 | # Delay(min) Outcome (successive runs). Note 1min/yr ~= 2ppm 106 | # 5 173 169 173 173 173 107 | # 10 171 173 171 108 | # 20 172 172 174 109 | # 40 173 172 173 Mean: 172.3 110 | # Note calibration factor is not saved on power down unless an RTC backup battery is used. An option is 111 | # to store the calibration factor on disk and issue rtc.calibration(factor) on boot. 112 | 113 | def getcal(self, minutes=5, cal=0, verbose=True): 114 | if d_series: 115 | return self._getcal_d(minutes, cal, verbose) 116 | verbose and print('Pyboard 1.x. Waiting {} minutes for calibration factor.'.format(minutes)) 117 | rtc.calibration(cal) # Clear existing cal 118 | self.save_time() # Set DS3231 from RTC 119 | self.await_transition() # Wait for DS3231 to change: on a 1 second boundary 120 | tus = utime.ticks_us() 121 | st = rtc.datetime()[7] 122 | while rtc.datetime()[7] == st: # Wait for RTC to change 123 | pass 124 | t1 = utime.ticks_diff(utime.ticks_us(), tus) # t1 is duration (μs) between DS and RTC change (start) 125 | rtcstart = get_ms(rtc.datetime()) # RTC start time in mS 126 | dsstart = utime.mktime(self.convert()) # DS start time in secs as recorded by await_transition 127 | 128 | utime.sleep(minutes * 60) 129 | 130 | self.await_transition() # DS second boundary 131 | tus = utime.ticks_us() 132 | st = rtc.datetime()[7] 133 | while rtc.datetime()[7] == st: 134 | pass 135 | t2 = utime.ticks_diff(utime.ticks_us(), tus) # t2 is duration (μs) between DS and RTC change (end) 136 | rtcend = get_ms(rtc.datetime()) 137 | dsend = utime.mktime(self.convert()) 138 | dsdelta = (dsend - dsstart) * 1000000 # Duration (μs) between DS edges as measured by DS3231 139 | if rtcend < rtcstart: # It's run past midnight. Assumption: run time < 1 day! 140 | rtcend += 24 * 3_600_000 141 | rtcdelta = (rtcend - rtcstart) * 1000 + t1 - t2 # Duration (μs) between DS edges as measured by RTC and corrected 142 | ppm = (1000000* (rtcdelta - dsdelta))/dsdelta 143 | if cal: 144 | verbose and print('Error {:4.1f}ppm {:4.1f}mins/year.'.format(ppm, ppm * 1.903)) 145 | return 0 146 | cal = int(-ppm / 0.954) 147 | verbose and print('Error {:4.1f}ppm {:4.1f}mins/year. Cal factor {}'.format(ppm, ppm * 1.903, cal)) 148 | return cal 149 | 150 | # Version for Pyboard D. This has μs resolution. 151 | def _getcal_d(self, minutes, cal, verbose): 152 | verbose and print('Pyboard D. Waiting {} minutes for calibration factor.'.format(minutes)) 153 | rtc.calibration(cal) # Clear existing cal 154 | self.save_time() # Set DS3231 from RTC 155 | self.await_transition() # Wait for DS3231 to change: on a 1 second boundary 156 | t = rtc.datetime() # Get RTC time 157 | # Time of DS3231 transition measured by RTC in μs since start of day 158 | rtc_start_us = get_us(t) 159 | dsstart = utime.mktime(self.convert()) # DS start time in secs 160 | 161 | utime.sleep(minutes * 60) 162 | 163 | self.await_transition() # Wait for DS second boundary 164 | t = rtc.datetime() 165 | # Time of DS3231 transition measured by RTC in μs since start of day 166 | rtc_end_us = get_us(t) 167 | dsend = utime.mktime(self.convert()) # DS end time in secs 168 | if rtc_end_us < rtc_start_us: # It's run past midnight. Assumption: run time < 1 day! 169 | rtc_end_us += 24 * 3_600_000_000 170 | 171 | dsdelta = (dsend - dsstart) * 1_000_000 # Duration (μs) between DS3231 edges as measured by DS3231 172 | rtcdelta = rtc_end_us - rtc_start_us # Duration (μs) between DS edges as measured by RTC 173 | ppm = (1_000_000 * (rtcdelta - dsdelta)) / dsdelta 174 | if cal: # We've already calibrated. Just report results. 175 | verbose and print('Error {:4.1f}ppm {:4.1f}mins/year.'.format(ppm, ppm * 1.903)) 176 | return 0 177 | cal = int(-ppm / 0.954) 178 | verbose and print('Error {:4.1f}ppm {:4.1f}mins/year. Cal factor {}'.format(ppm, ppm * 1.903, cal)) 179 | return cal 180 | 181 | def calibrate(self, minutes=5): 182 | cal = self.getcal(minutes) 183 | rtc.calibration(cal) 184 | print('Pyboard RTC is calibrated. Factor is {}.'.format(cal)) 185 | return cal 186 | -------------------------------------------------------------------------------- /DS3231/ds3231_port.py: -------------------------------------------------------------------------------- 1 | # ds3231_port.py Portable driver for DS3231 precison real time clock. 2 | # Adapted from WiPy driver at https://github.com/scudderfish/uDS3231 3 | 4 | # Author: Peter Hinch 5 | # Copyright Peter Hinch 2018 Released under the MIT license. 6 | 7 | import utime 8 | import machine 9 | import sys 10 | DS3231_I2C_ADDR = 104 11 | 12 | try: 13 | rtc = machine.RTC() 14 | except: 15 | print('Warning: machine module does not support the RTC.') 16 | rtc = None 17 | 18 | def bcd2dec(bcd): 19 | return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f)) 20 | 21 | def dec2bcd(dec): 22 | tens, units = divmod(dec, 10) 23 | return (tens << 4) + units 24 | 25 | def tobytes(num): 26 | return num.to_bytes(1, 'little') 27 | 28 | class DS3231: 29 | def __init__(self, i2c): 30 | self.ds3231 = i2c 31 | self.timebuf = bytearray(7) 32 | if DS3231_I2C_ADDR not in self.ds3231.scan(): 33 | raise RuntimeError("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR) 34 | 35 | def get_time(self, set_rtc=False): 36 | if set_rtc: 37 | self.await_transition() # For accuracy set RTC immediately after a seconds transition 38 | else: 39 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) # don't wait 40 | return self.convert(set_rtc) 41 | 42 | def convert(self, set_rtc=False): # Return a tuple in localtime() format (less yday) 43 | data = self.timebuf 44 | ss = bcd2dec(data[0]) 45 | mm = bcd2dec(data[1]) 46 | if data[2] & 0x40: 47 | hh = bcd2dec(data[2] & 0x1f) 48 | if data[2] & 0x20: 49 | hh += 12 50 | else: 51 | hh = bcd2dec(data[2]) 52 | wday = data[3] 53 | DD = bcd2dec(data[4]) 54 | MM = bcd2dec(data[5] & 0x1f) 55 | YY = bcd2dec(data[6]) 56 | if data[5] & 0x80: 57 | YY += 2000 58 | else: 59 | YY += 1900 60 | # Time from DS3231 in time.localtime() format (less yday) 61 | result = YY, MM, DD, hh, mm, ss, wday -1, 0 62 | if set_rtc: 63 | if rtc is None: 64 | # Best we can do is to set local time 65 | secs = utime.mktime(result) 66 | utime.localtime(secs) 67 | else: 68 | rtc.datetime((YY, MM, DD, wday, hh, mm, ss, 0)) 69 | return result 70 | 71 | def save_time(self): 72 | (YY, MM, mday, hh, mm, ss, wday, yday) = utime.localtime() # Based on RTC 73 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss))) 74 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm))) 75 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode 76 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday 77 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month 78 | if YY >= 2000: 79 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM) | 0b10000000)) # Century bit 80 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-2000))) 81 | else: 82 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM))) 83 | self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900))) 84 | 85 | # Wait until DS3231 seconds value changes before reading and returning data 86 | def await_transition(self): 87 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 88 | ss = self.timebuf[0] 89 | while ss == self.timebuf[0]: 90 | self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) 91 | return self.timebuf 92 | 93 | # Test hardware RTC against DS3231. Default runtime 10 min. Return amount 94 | # by which DS3231 clock leads RTC in PPM or seconds per year. 95 | # Precision is achieved by starting and ending the measurement on DS3231 96 | # one-seond boundaries and using ticks_ms() to time the RTC. 97 | # For a 10 minute measurement +-1ms corresponds to 1.7ppm or 53s/yr. Longer 98 | # runtimes improve this, but the DS3231 is "only" good for +-2ppm over 0-40C. 99 | def rtc_test(self, runtime=600, ppm=False, verbose=True): 100 | if rtc is None: 101 | raise RuntimeError('machine.RTC does not exist') 102 | verbose and print('Waiting {} minutes for result'.format(runtime//60)) 103 | factor = 1_000_000 if ppm else 114_155_200 # seconds per year 104 | 105 | self.await_transition() # Start on transition of DS3231. Record time in .timebuf 106 | t = utime.ticks_ms() # Get system time now 107 | ss = rtc.datetime()[6] # Seconds from system RTC 108 | while ss == rtc.datetime()[6]: 109 | pass 110 | ds = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC 111 | ds3231_start = utime.mktime(self.convert()) # Time when transition occurred 112 | t = rtc.datetime() 113 | rtc_start = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0 114 | 115 | utime.sleep(runtime) # Wait a while (precision doesn't matter) 116 | 117 | self.await_transition() # of DS3231 and record the time 118 | t = utime.ticks_ms() # and get system time now 119 | ss = rtc.datetime()[6] # Seconds from system RTC 120 | while ss == rtc.datetime()[6]: 121 | pass 122 | de = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC 123 | ds3231_end = utime.mktime(self.convert()) # Time when transition occurred 124 | t = rtc.datetime() 125 | rtc_end = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0 126 | 127 | d_rtc = 1000 * (rtc_end - rtc_start) + de - ds # ms recorded by RTC 128 | d_ds3231 = 1000 * (ds3231_end - ds3231_start) # ms recorded by DS3231 129 | ratio = (d_ds3231 - d_rtc) / d_ds3231 130 | ppm = ratio * 1_000_000 131 | verbose and print('DS3231 leads RTC by {:4.1f}ppm {:4.1f}mins/yr'.format(ppm, ppm*1.903)) 132 | return ratio * factor 133 | 134 | 135 | def _twos_complement(self, input_value: int, num_bits: int) -> int: 136 | mask = 2 ** (num_bits - 1) 137 | return -(input_value & mask) + (input_value & ~mask) 138 | 139 | 140 | def get_temperature(self): 141 | t = self.ds3231.readfrom_mem(DS3231_I2C_ADDR, 0x11, 2) 142 | i = t[0] << 8 | t[1] 143 | return self._twos_complement(i >> 6, 10) * 0.25 144 | -------------------------------------------------------------------------------- /DS3231/ds3231_port_test.py: -------------------------------------------------------------------------------- 1 | # ds3231_port_test 2 | # Test/demo of portable driver for DS3231 precision RTC chip 3 | 4 | # Author: Peter Hinch 5 | # Copyright Peter Hinch 2018 Released under the MIT license 6 | 7 | from machine import Pin, I2C 8 | import utime 9 | import sys 10 | import uos 11 | from ds3231_port import DS3231 12 | 13 | # If powering the DS3231 from a Pyboard D 3V3 output: 14 | if uos.uname().machine.split(' ')[0][:4] == 'PYBD': 15 | Pin.board.EN_3V3.value(1) 16 | 17 | # A Pyboard test 18 | #from pyb import RTC 19 | #rtc = RTC() 20 | #rtc.datetime((2018, 1, 1, 1, 12, 0, 0, 0)) # Force incorrect setting 21 | 22 | # mode and pull are specified in case pullups are absent. 23 | # The pin ID's are arbitrary. 24 | if sys.platform == 'pyboard': 25 | scl_pin = Pin('X2', pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) 26 | sda_pin = Pin('X1', pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) 27 | else: # I tested on ESP32 28 | scl_pin = Pin(19, pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) 29 | sda_pin = Pin(18, pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) 30 | 31 | i2c = I2C(-1, scl=scl_pin, sda=sda_pin) 32 | ds3231 = DS3231(i2c) 33 | 34 | print('Initial values') 35 | print('DS3231 time:', ds3231.get_time()) 36 | print('RTC time: ', utime.localtime()) 37 | 38 | print('Setting DS3231 from RTC') 39 | ds3231.save_time() # Set DS3231 from RTC 40 | print('DS3231 time:', ds3231.get_time()) 41 | print('RTC time: ', utime.localtime()) 42 | 43 | print('Running RTC test for 2 mins') 44 | print('RTC leads DS3231 by', ds3231.rtc_test(120, True), 'ppm') 45 | -------------------------------------------------------------------------------- /DS3231/ds3231_test.py: -------------------------------------------------------------------------------- 1 | # Program to test/demonstrate consistency of results from getcal() 2 | from array import array 3 | from ds3231_pb import DS3231 4 | 5 | # This takes 12.5 hours to run: to sve you the trouble here are results from one sample of Pyboard. 6 | 7 | # Mean and standard deviation of RTC correction factors based on ten runs over different periods. 8 | # Conclusion: for the best possible accuracy, run for 20 minutes. However a ten minute run gave 9 | # a result within 2ppm (one minute/yr). 10 | # >>> test() 11 | # t = 5 -174 -175 -175 -172 -175 -175 -172 -174 -175 -172 avg -173.8 sd 1.3 12 | # t = 10 -175 -175 -175 -175 -173 -175 -176 -175 -174 -175 avg -174.8 sd 0.7 13 | # t = 20 -175 -175 -175 -175 -174 -175 -175 -175 -175 -174 avg -174.8 sd 0.4 14 | # t = 40 -175 -175 -175 -174 -174 -175 -174 -174 -175 -174 avg -174.4 sd 0.5 15 | 16 | def test(): 17 | NSAMPLES = 10 18 | a = DS3231() 19 | for t in (5, 10, 20, 40): 20 | values = array('f', (0 for z in range(NSAMPLES))) 21 | print('t = {:2d}'.format(t), end = '') 22 | for x in range(NSAMPLES): 23 | cal = a.getcal(t) 24 | values[x] = cal 25 | print('{:5d}'.format(cal), end = '') 26 | avg = sum(values)/NSAMPLES 27 | sd2 = sum([(v -avg)**2 for v in values])/NSAMPLES 28 | sd = sd2 ** 0.5 29 | print(' avg {:5.1f} sd {:5.1f}'.format(avg, sd)) 30 | -------------------------------------------------------------------------------- /ESP32/ESP32-Devkit-C-pinout.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/ESP32/ESP32-Devkit-C-pinout.pdf -------------------------------------------------------------------------------- /ESP8266/benchmark.py: -------------------------------------------------------------------------------- 1 | # benchmark.py Simple benchmark for umqtt.simple 2 | # Assumes simple.py (from micropython-lib) is copied to ESP8266 3 | # Outcome with mosquitto running on a Raspberry Pi on wired network, 4 | # Wemos D1 Mini running on WiFi: echo received in max 154 ms min 27 ms 5 | 6 | import ubinascii 7 | from simple import MQTTClient 8 | from machine import unique_id 9 | from utime import sleep, ticks_ms, ticks_diff 10 | 11 | def tdiff(): 12 | new_semantics = ticks_diff(2, 1) == 1 13 | def func(old, new): 14 | nonlocal new_semantics 15 | if new_semantics: 16 | return ticks_diff(new, old) 17 | return ticks_diff(old, new) 18 | return func 19 | 20 | ticksdiff = tdiff() 21 | 22 | SERVER = "192.168.0.23" 23 | CLIENT_ID = ubinascii.hexlify(unique_id()) 24 | TOPIC = b"led" 25 | QOS = 1 26 | 27 | t = 0 28 | maxt = 0 29 | mint = 5000 30 | 31 | 32 | def sub_cb(topic, msg): 33 | global t, maxt, mint 34 | dt = ticksdiff(t, ticks_ms()) 35 | print('echo received in {} ms'.format(dt)) 36 | print((topic, msg)) 37 | maxt = max(maxt, dt) 38 | mint = min(mint, dt) 39 | 40 | 41 | def main(quit=True): 42 | global t 43 | c = MQTTClient(CLIENT_ID, SERVER) 44 | # Subscribed messages will be delivered to this callback 45 | c.set_callback(sub_cb) 46 | c.connect() 47 | c.subscribe(TOPIC, qos = QOS) 48 | print("Connected to %s, subscribed to %s topic" % (SERVER, TOPIC)) 49 | n = 0 50 | pubs = 0 51 | try: 52 | while 1: 53 | n += 1 54 | if not n % 100: 55 | t = ticks_ms() 56 | c.publish(TOPIC, str(pubs).encode('UTF8'), retain = False, qos = QOS) 57 | c.wait_msg() 58 | pubs += 1 59 | if not pubs % 100: 60 | print('echo received in max {} ms min {} ms'. 61 | format(maxt, mint)) 62 | if quit: 63 | return 64 | sleep(0.05) 65 | c.check_msg() 66 | finally: 67 | c.disconnect() 68 | -------------------------------------------------------------------------------- /ESP8266/conn.py: -------------------------------------------------------------------------------- 1 | # Connect in station mode. Use saved parameters if possible to save flash wear 2 | 3 | import network 4 | import utime 5 | 6 | use_default = True 7 | ssid = 'my_ssid' 8 | pw = 'my_password' 9 | 10 | sta_if = network.WLAN(network.STA_IF) 11 | if use_default: 12 | secs = 5 13 | while secs >= 0 and not sta_if.isconnected(): 14 | utime.sleep(1) 15 | secs -= 1 16 | 17 | # If can't use default, use specified LAN 18 | if not sta_if.isconnected(): 19 | sta_if.active(True) 20 | sta_if.connect(ssid, pw) 21 | while not sta_if.isconnected(): 22 | utime.sleep(1) 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Peter Hinch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PICOWEB.md: -------------------------------------------------------------------------------- 1 | # 1. Running Picoweb on hardware devices 2 | 3 | This has regularly caused dificulty on the forum. 4 | 5 | The target hardware is assumed to be running official MicroPython firmware. This 6 | has now been tested with a daily build of official firmware which now includes 7 | uasyncio V3. It is therefore compatible with official uasyncio V2 and V3. 8 | 9 | This repo aims to clarify the installation process. Paul Sokolovsky's Picoweb 10 | code is unchanged. The demos are trivially changed to use IP '0.0.0.0' and port 11 | 80. 12 | 13 | The Picoweb version in this repo is no longer current, and subsequent versions 14 | are no longer compatible with official firmware. See [Picoweb versions](./PICOWEB.md#5-picoweb-versions) if you 15 | want to run a version newer than that supplied here. 16 | 17 | Note that the ESP8266 requires the use of frozen bytecode: see [ESP8266](./PICOWEB.md#3-ESP8266) 18 | for installation instructions. 19 | 20 | On other platfroms two ways of installing Picoweb are available: copying this 21 | directory to the target or using `upip`. To use `upip` you should ensure your 22 | firmware is V1.11 or later; your target will also require an internet 23 | connection. Both methods require the following preliminaries. 24 | 25 | ## 1.1 Preliminary steps 26 | 27 | ### 1.1.1 Clone this repo to your PC 28 | 29 | From a suitable destination directory issue 30 | ``` 31 | git clone https://github.com/peterhinch/micropython-samples 32 | ``` 33 | 34 | ### 1.1.2 Establish uasyncio status 35 | 36 | Determine whether your target has `uasyncio` already installed. At the REPL 37 | issue: 38 | ``` 39 | >>> import uasyncio 40 | >>> 41 | ``` 42 | If this throws an `ImportError`, `uasyncio` is not installed. 43 | 44 | ## 1.2 Installing using upip 45 | 46 | Copy the `picoweb` subdirectory of this repo's `PicoWeb` directory, with its 47 | contents, to the target. If using `rshell` to connect to a Pyboard D this would 48 | be done from the `PicoWeb` directory with: 49 | ``` 50 | /my/tree/PicoWeb> cp -r picoweb/ /flash 51 | ``` 52 | 53 | Ensure your target is connected to the internet. Then perform the following 54 | steps. The first step may be omitted if `uasyncio` is already installed. 55 | ``` 56 | upip.install('micropython-uasyncio') 57 | upip.install('micropython-ulogging') 58 | upip.install('micropython-pkg_resources') 59 | upip.install('utemplate') 60 | ``` 61 | 62 | ## 1.3 Installing by copying this archive 63 | 64 | Copy the contents of the `PicoWeb` directory (including subdirectories) to the 65 | target. If using `rshell` on an ESP32 change to this directory, at the `rshell` 66 | prompt issue 67 | ``` 68 | /my/tree/PicoWeb> rsync . /pyboard 69 | ``` 70 | This may take some time: 1 minute here on ESP32. 71 | 72 | If `uasyncio` was already installed, the corrsponding directory on the target 73 | may be removed. 74 | 75 | # 2. Running Picoweb 76 | 77 | At the REPL connect to the network and determine your IP address 78 | ``` 79 | >>> import network 80 | >>> w = network.WLAN() 81 | >>> w.ifconfig() 82 | ``` 83 | 84 | issue 85 | ``` 86 | >>> from picoweb import example_webapp 87 | ``` 88 | 89 | or 90 | ``` 91 | >>> from picoweb import example_webapp2 92 | ``` 93 | 94 | Then point your browser at the IP address determined above. 95 | 96 | # 3. ESP8266 97 | 98 | RAM limitations require the use of frozen bytecode, and getting the examples 99 | running is a little more involved. Create a directory on your PC and copy the 100 | contents of this directory to it. Then add the files `inisetup.py`, `_boot.py` 101 | and `flashbdev.py` which may be found in the MicroPython source tree under 102 | `ports/esp8266/modules`. You may also want to add a custom connect module to 103 | simplify connection to your WiFi. Then build the firmware. The script I used 104 | was 105 | ```bash 106 | #! /bin/bash 107 | 108 | # Test picoweb on ESP8266 109 | 110 | DIRECTORY='/home/adminpete/temp/picoweb' 111 | 112 | cd /mnt/qnap2/data/Projects/MicroPython/micropython/ports/esp8266 113 | 114 | make clean 115 | esptool.py --port /dev/ttyUSB0 erase_flash 116 | 117 | if make -j 8 FROZEN_MPY_DIR=$DIRECTORY 118 | then 119 | sleep 1 120 | esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash --flash_size=detect -fm dio 0 build/firmware-combined.bin 121 | sleep 4 122 | rshell -p /dev/ttyUSB0 --buffer-size=30 --editor nano 123 | else 124 | echo Build failure 125 | fi 126 | ``` 127 | For the demos you will need to make the `example_webapp.py` source file and 128 | `squares.tpl` accessible in the filesystem. The following `rshell` commands, 129 | executed from this directory or the one created above, will make these 130 | available. 131 | ``` 132 | path/to/repo> mkdir /pyboard/picoweb 133 | path/to/repo> mkdir /pyboard/picoweb/templates 134 | path/to/repo> cp picoweb/example_webapp.py /pyboard/picoweb/ 135 | path/to/repo> cp picoweb/templates/squares.tpl /pyboard/picoweb/templates/ 136 | ``` 137 | 138 | 139 | # 4. Documentation and further examples 140 | 141 | See [the PicoWeb docs](https://github.com/pfalcon/picoweb) 142 | 143 | Note that to run these demos on platforms other than the Unix build you may 144 | want to change IP and port as above. 145 | 146 | # 5. Picoweb versions 147 | 148 | At the time of writing (Sept 2019) Paul Sokolovsky has released an update to 149 | support Unicode strings. This is incompatible with official firmware. The 150 | difference is small and easily fixed, however it is likely that future versions 151 | will acquire further incompatibilities. I do not plan to maintain a compatible 152 | `Picoweb-copy` fork; hopefully someone will accept this task. 153 | 154 | The issues are discussed [in this forum thread](https://forum.micropython.org/viewtopic.php?f=18&t=6002). 155 | 156 | Options are: 157 | 1. Use the version in this repo. 158 | 2. Use the latest version and adapt it. 159 | 3. Use the Pycopy firmware (requires compilation). 160 | 4. Abandon Picoweb and use an alternative web framework. 161 | 162 | These are discussed in detail in the above forum thread. 163 | -------------------------------------------------------------------------------- /PicoWeb/picoweb/example_webapp.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is a picoweb example showing a centralized web page route 3 | # specification (classical Django style). 4 | # 5 | import ure as re 6 | import picoweb 7 | 8 | 9 | def index(req, resp): 10 | # You can construct an HTTP response completely yourself, having 11 | # a full control of headers sent... 12 | yield from resp.awrite("HTTP/1.0 200 OK\r\n") 13 | yield from resp.awrite("Content-Type: text/html\r\n") 14 | yield from resp.awrite("\r\n") 15 | yield from resp.awrite("I can show you a table of squares.
") 16 | yield from resp.awrite("Or my source.
") 17 | yield from resp.awrite("Or enter /iam/Mickey Mouse after the URL for regexp match.") 18 | 19 | 20 | def squares(req, resp): 21 | # Or can use a convenience function start_response() (see its source for 22 | # extra params it takes). 23 | yield from picoweb.start_response(resp) 24 | yield from app.render_template(resp, "squares.tpl", (req,)) 25 | 26 | 27 | def hello(req, resp): 28 | yield from picoweb.start_response(resp) 29 | # Here's how you extract matched groups from a regex URI match 30 | yield from resp.awrite("Hello " + req.url_match.group(1)) 31 | 32 | 33 | ROUTES = [ 34 | # You can specify exact URI string matches... 35 | ("/", index), 36 | ("/squares", squares), 37 | ("/file", lambda req, resp: (yield from app.sendfile(resp, "example_webapp.py"))), 38 | # ... or match using a regex, the match result available as req.url_match 39 | # for match group extraction in your view. 40 | (re.compile("^/iam/(.+)"), hello), 41 | ] 42 | 43 | 44 | import ulogging as logging 45 | logging.basicConfig(level=logging.INFO) 46 | #logging.basicConfig(level=logging.DEBUG) 47 | 48 | app = picoweb.WebApp(__name__, ROUTES) 49 | # debug values: 50 | # -1 disable all logging 51 | # 0 (False) normal logging: requests and errors 52 | # 1 (True) debug logging 53 | # 2 extra debug logging 54 | app.run(debug=1, host='0.0.0.0', port=80) 55 | 56 | -------------------------------------------------------------------------------- /PicoWeb/picoweb/example_webapp2.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is a picoweb example showing a web page route 3 | # specification using view decorators (Flask style). 4 | # 5 | import picoweb 6 | 7 | 8 | app = picoweb.WebApp(__name__) 9 | 10 | @app.route("/") 11 | def index(req, resp): 12 | yield from picoweb.start_response(resp) 13 | yield from resp.awrite("I can show you a table of squares.") 14 | 15 | @app.route("/squares") 16 | def squares(req, resp): 17 | yield from picoweb.start_response(resp) 18 | yield from app.render_template(resp, "squares.tpl", (req,)) 19 | 20 | 21 | import ulogging as logging 22 | logging.basicConfig(level=logging.INFO) 23 | 24 | app.run(debug=True, host='0.0.0.0', port=80) 25 | 26 | -------------------------------------------------------------------------------- /PicoWeb/picoweb/templates/squares.tpl: -------------------------------------------------------------------------------- 1 | {% args req %} 2 | 3 | Request path: '{{req.path}}'
4 | 5 | {% for i in range(5) %} 6 | 7 | {% endfor %} 8 |
{{i}} {{"%2d" % i ** 2}}
9 | 10 | -------------------------------------------------------------------------------- /PicoWeb/picoweb/utils.py: -------------------------------------------------------------------------------- 1 | def unquote_plus(s): 2 | # TODO: optimize 3 | s = s.replace("+", " ") 4 | arr = s.split("%") 5 | arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]] 6 | return arr[0] + "".join(arr2) 7 | 8 | def parse_qs(s): 9 | res = {} 10 | if s: 11 | pairs = s.split("&") 12 | for p in pairs: 13 | vals = [unquote_plus(x) for x in p.split("=", 1)] 14 | if len(vals) == 1: 15 | vals.append(True) 16 | old = res.get(vals[0]) 17 | if old is not None: 18 | if not isinstance(old, list): 19 | old = [old] 20 | res[vals[0]] = old 21 | old.append(vals[1]) 22 | else: 23 | res[vals[0]] = vals[1] 24 | return res 25 | 26 | #print(parse_qs("foo")) 27 | #print(parse_qs("fo%41o+bar=+++1")) 28 | #print(parse_qs("foo=1&foo=2")) 29 | -------------------------------------------------------------------------------- /PicoWeb/pkg_resources.py: -------------------------------------------------------------------------------- 1 | import uio 2 | 3 | c = {} 4 | 5 | def resource_stream(package, resource): 6 | if package not in c: 7 | try: 8 | if package: 9 | p = __import__(package + ".R", None, None, True) 10 | else: 11 | p = __import__("R") 12 | c[package] = p.R 13 | except ImportError: 14 | if package: 15 | p = __import__(package) 16 | d = p.__path__ 17 | else: 18 | d = "." 19 | # if d[0] != "/": 20 | # import uos 21 | # d = uos.getcwd() + "/" + d 22 | c[package] = d + "/" 23 | 24 | p = c[package] 25 | if isinstance(p, dict): 26 | return uio.BytesIO(p[resource]) 27 | return open(p + resource, "rb") 28 | -------------------------------------------------------------------------------- /PicoWeb/ulogging.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | CRITICAL = 50 4 | ERROR = 40 5 | WARNING = 30 6 | INFO = 20 7 | DEBUG = 10 8 | NOTSET = 0 9 | 10 | _level_dict = { 11 | CRITICAL: "CRIT", 12 | ERROR: "ERROR", 13 | WARNING: "WARN", 14 | INFO: "INFO", 15 | DEBUG: "DEBUG", 16 | } 17 | 18 | _stream = sys.stderr 19 | 20 | class Logger: 21 | 22 | level = NOTSET 23 | 24 | def __init__(self, name): 25 | self.name = name 26 | 27 | def _level_str(self, level): 28 | l = _level_dict.get(level) 29 | if l is not None: 30 | return l 31 | return "LVL%s" % level 32 | 33 | def setLevel(self, level): 34 | self.level = level 35 | 36 | def isEnabledFor(self, level): 37 | return level >= (self.level or _level) 38 | 39 | def log(self, level, msg, *args): 40 | if level >= (self.level or _level): 41 | _stream.write("%s:%s:" % (self._level_str(level), self.name)) 42 | if not args: 43 | print(msg, file=_stream) 44 | else: 45 | print(msg % args, file=_stream) 46 | 47 | def debug(self, msg, *args): 48 | self.log(DEBUG, msg, *args) 49 | 50 | def info(self, msg, *args): 51 | self.log(INFO, msg, *args) 52 | 53 | def warning(self, msg, *args): 54 | self.log(WARNING, msg, *args) 55 | 56 | def error(self, msg, *args): 57 | self.log(ERROR, msg, *args) 58 | 59 | def critical(self, msg, *args): 60 | self.log(CRITICAL, msg, *args) 61 | 62 | def exc(self, e, msg, *args): 63 | self.log(ERROR, msg, *args) 64 | sys.print_exception(e, _stream) 65 | 66 | def exception(self, msg, *args): 67 | self.exc(sys.exc_info()[1], msg, *args) 68 | 69 | 70 | _level = INFO 71 | _loggers = {} 72 | 73 | def getLogger(name): 74 | if name in _loggers: 75 | return _loggers[name] 76 | l = Logger(name) 77 | _loggers[name] = l 78 | return l 79 | 80 | def info(msg, *args): 81 | getLogger(None).info(msg, *args) 82 | 83 | def debug(msg, *args): 84 | getLogger(None).debug(msg, *args) 85 | 86 | def basicConfig(level=INFO, filename=None, stream=None, format=None): 87 | global _level, _stream 88 | _level = level 89 | if stream: 90 | _stream = stream 91 | if filename is not None: 92 | print("logging.basicConfig: filename arg is not supported") 93 | if format is not None: 94 | print("logging.basicConfig: format arg is not supported") 95 | -------------------------------------------------------------------------------- /PicoWeb/utemplate/compiled.py: -------------------------------------------------------------------------------- 1 | class Loader: 2 | 3 | def __init__(self, pkg, dir): 4 | if dir == ".": 5 | dir = "" 6 | else: 7 | dir = dir.replace("/", ".") + "." 8 | if pkg and pkg != "__main__": 9 | dir = pkg + "." + dir 10 | self.p = dir 11 | 12 | def load(self, name): 13 | name = name.replace(".", "_") 14 | return __import__(self.p + name, None, None, (name,)).render 15 | -------------------------------------------------------------------------------- /PicoWeb/utemplate/source.py: -------------------------------------------------------------------------------- 1 | # os module is loaded on demand 2 | #import os 3 | 4 | from . import compiled 5 | 6 | 7 | class Compiler: 8 | 9 | START_CHAR = "{" 10 | STMNT = "%" 11 | STMNT_END = "%}" 12 | EXPR = "{" 13 | EXPR_END = "}}" 14 | 15 | def __init__(self, file_in, file_out, indent=0, seq=0, loader=None): 16 | self.file_in = file_in 17 | self.file_out = file_out 18 | self.loader = loader 19 | self.seq = seq 20 | self._indent = indent 21 | self.stack = [] 22 | self.in_literal = False 23 | self.flushed_header = False 24 | self.args = "*a, **d" 25 | 26 | def indent(self, adjust=0): 27 | if not self.flushed_header: 28 | self.flushed_header = True 29 | self.indent() 30 | self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args)) 31 | self.stack.append("def") 32 | self.file_out.write(" " * (len(self.stack) + self._indent + adjust)) 33 | 34 | def literal(self, s): 35 | if not s: 36 | return 37 | if not self.in_literal: 38 | self.indent() 39 | self.file_out.write('yield """') 40 | self.in_literal = True 41 | self.file_out.write(s.replace('"', '\\"')) 42 | 43 | def close_literal(self): 44 | if self.in_literal: 45 | self.file_out.write('"""\n') 46 | self.in_literal = False 47 | 48 | def render_expr(self, e): 49 | self.indent() 50 | self.file_out.write('yield str(' + e + ')\n') 51 | 52 | def parse_statement(self, stmt): 53 | tokens = stmt.split(None, 1) 54 | if tokens[0] == "args": 55 | if len(tokens) > 1: 56 | self.args = tokens[1] 57 | else: 58 | self.args = "" 59 | elif tokens[0] == "set": 60 | self.indent() 61 | self.file_out.write(stmt[3:].strip() + "\n") 62 | elif tokens[0] == "include": 63 | if not self.flushed_header: 64 | # If there was no other output, we still need a header now 65 | self.indent() 66 | tokens = tokens[1].split(None, 1) 67 | args = "" 68 | if len(tokens) > 1: 69 | args = tokens[1] 70 | if tokens[0][0] == "{": 71 | self.indent() 72 | # "1" as fromlist param is uPy hack 73 | self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2]) 74 | self.indent() 75 | self.file_out.write("yield from _.render(%s)\n" % args) 76 | return 77 | 78 | with self.loader.input_open(tokens[0][1:-1]) as inc: 79 | self.seq += 1 80 | c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq) 81 | inc_id = self.seq 82 | self.seq = c.compile() 83 | self.indent() 84 | self.file_out.write("yield from render%d(%s)\n" % (inc_id, args)) 85 | elif len(tokens) > 1: 86 | if tokens[0] == "elif": 87 | assert self.stack[-1] == "if" 88 | self.indent(-1) 89 | self.file_out.write(stmt + ":\n") 90 | else: 91 | self.indent() 92 | self.file_out.write(stmt + ":\n") 93 | self.stack.append(tokens[0]) 94 | else: 95 | if stmt.startswith("end"): 96 | assert self.stack[-1] == stmt[3:] 97 | self.stack.pop(-1) 98 | elif stmt == "else": 99 | assert self.stack[-1] == "if" 100 | self.indent(-1) 101 | self.file_out.write("else:\n") 102 | else: 103 | assert False 104 | 105 | def parse_line(self, l): 106 | while l: 107 | start = l.find(self.START_CHAR) 108 | if start == -1: 109 | self.literal(l) 110 | return 111 | self.literal(l[:start]) 112 | self.close_literal() 113 | sel = l[start + 1] 114 | #print("*%s=%s=" % (sel, EXPR)) 115 | if sel == self.STMNT: 116 | end = l.find(self.STMNT_END) 117 | assert end > 0 118 | stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip() 119 | self.parse_statement(stmt) 120 | end += len(self.STMNT_END) 121 | l = l[end:] 122 | if not self.in_literal and l == "\n": 123 | break 124 | elif sel == self.EXPR: 125 | # print("EXPR") 126 | end = l.find(self.EXPR_END) 127 | assert end > 0 128 | expr = l[start + len(self.START_CHAR + self.EXPR):end].strip() 129 | self.render_expr(expr) 130 | end += len(self.EXPR_END) 131 | l = l[end:] 132 | else: 133 | self.literal(l[start]) 134 | l = l[start + 1:] 135 | 136 | def header(self): 137 | self.file_out.write("# Autogenerated file\n") 138 | 139 | def compile(self): 140 | self.header() 141 | for l in self.file_in: 142 | self.parse_line(l) 143 | self.close_literal() 144 | return self.seq 145 | 146 | 147 | class Loader(compiled.Loader): 148 | 149 | def __init__(self, pkg, dir): 150 | super().__init__(pkg, dir) 151 | self.dir = dir 152 | if pkg == "__main__": 153 | # if pkg isn't really a package, don't bother to use it 154 | # it means we're running from "filesystem directory", not 155 | # from a package. 156 | pkg = None 157 | 158 | self.pkg_path = "" 159 | if pkg: 160 | p = __import__(pkg) 161 | if isinstance(p.__path__, str): 162 | # uPy 163 | self.pkg_path = p.__path__ 164 | else: 165 | # CPy 166 | self.pkg_path = p.__path__[0] 167 | self.pkg_path += "/" 168 | 169 | def input_open(self, template): 170 | path = self.pkg_path + self.dir + "/" + template 171 | return open(path) 172 | 173 | def compiled_path(self, template): 174 | return self.dir + "/" + template.replace(".", "_") + ".py" 175 | 176 | def load(self, name): 177 | try: 178 | return super().load(name) 179 | except (OSError, ImportError): 180 | pass 181 | 182 | compiled_path = self.pkg_path + self.compiled_path(name) 183 | 184 | f_in = self.input_open(name) 185 | f_out = open(compiled_path, "w") 186 | c = Compiler(f_in, f_out, loader=self) 187 | c.compile() 188 | f_in.close() 189 | f_out.close() 190 | return super().load(name) 191 | -------------------------------------------------------------------------------- /RSHELL_MACROS.md: -------------------------------------------------------------------------------- 1 | # The rshell macro fork 2 | 3 | These notes describe one of a number of possible ways to use the macro 4 | facility. 5 | 6 | The aim was to provide a set of generic macros, along with a project specific 7 | set. The starting point is an alias, run before work is started on a given 8 | project. The aliases are created in `~/.bashrc`. The example below is for two 9 | projects, `mqtt` and `asyn`. 10 | ```bash 11 | alias asyn='export PROJECT=ASYN;cd /MicroPython/micropython-async/' 12 | alias mqtt='export PROJECT=MQTT; cd /MicroPython/micropython-mqtt' 13 | ``` 14 | The `PROJECT` environment variable is examined by the file `rshell_macros.py` 15 | located in the `rshell` directory. An example is presented here: 16 | ```python 17 | import os 18 | proj = None 19 | try: 20 | proj = os.environ['PROJECT'] 21 | except KeyError: 22 | print('Environment var PROJECT not found: only generic macros loaded.') 23 | 24 | macros = {} 25 | macros['..'] = 'cd ..' 26 | macros['...'] = 'cd ../..' 27 | macros['ll'] = 'ls -al {}', 'List any directory' 28 | macros['lf'] = 'ls -al /flash/{}' 29 | macros['lsd'] = 'ls -al /sd/{}' 30 | macros['lpb'] = 'ls -al /pyboard/{}' 31 | macros['mv'] = 'cp {0} {1}; rm {0}', 'Move/rename' 32 | macros['repl'] = 'repl ~ import machine ~ machine.soft_reset()', 'Clean REPL' 33 | macros['up'] = 'cd /MicroPython' 34 | macros['asyn'] = 'cd /MicroPython/micropython-async' 35 | macros['primitives'] = 'cd /MicroPython/micropython-async/v3; rsync primitives/ {}/primitives; cd -', 'Copy V3 primitives to dest' 36 | 37 | if proj == 'MQTT': 38 | print('Importing macros for MQTT') 39 | macros['home'] = 'cd /MicroPython/micropython-mqtt/mqtt_as' 40 | elif proj == 'ASYN': 41 | print('Importing macros for ASYN') 42 | macros['home'] = 'cd /MicroPython/micropython-async' 43 | macros['v3'] = 'cd /MicroPython/micropython-async/v3' 44 | macros['off'] = 'cd /MicroPython/micropython-async/official', 'official uasyncio' 45 | macros['sync'] = 'cd /MicroPython/micropython-async/v3; rsync primitives/ {}/primitives', 'Copy V3 primitives to dest' 46 | macros['demos'] = 'cd /MicroPython/micropython-async/v3; rsync as_demos/ {}/as_demos', 'Copy V3 demos to dest' 47 | macros['drivers'] = 'cd /MicroPython/micropython-async/v3; rsync as_drivers/ {}/as_drivers', 'Copy V3 drivers to dest' 48 | ``` 49 | The first part of the script defines generic macros such as `ll` and `lsd` 50 | available to any project, and if no project is defined. 51 | 52 | Project specific macros are added in response to the environment variable. 53 | Note the way args are passed: to update the SD card on a Pyboard the macro 54 | `primitives` would be called with: 55 | ``` 56 | MicroPython > m primitives /sd 57 | ``` 58 | The `mv` macro, called with 59 | ``` 60 | MicroPython > m mv source_path dest_path 61 | ``` 62 | expands to 63 | ``` 64 | cp source_path dest_path; rm source_path 65 | ``` 66 | -------------------------------------------------------------------------------- /astronomy/CDR_license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/astronomy/CDR_license.txt -------------------------------------------------------------------------------- /astronomy/lunartick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/astronomy/lunartick.jpg -------------------------------------------------------------------------------- /astronomy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["sched/sun_moon.py", "github:peterhinch/micropython-samples/astronomy/sun_moon.py"], 4 | ["sched/sun_moon_test.py", "github:peterhinch/micropython-samples/astronomy/sun_moon_test.py"], 5 | ["sched/moonphase.py", "github:peterhinch/micropython-samples/astronomy/moonphase.py"] 6 | ], 7 | "version": "0.1" 8 | } 9 | -------------------------------------------------------------------------------- /astronomy/sun_moon_test.py: -------------------------------------------------------------------------------- 1 | # sun_moon_test.py Test script for sun_moon.py 2 | 3 | # Copyright (c) Peter Hinch 2023 4 | # Released under the MIT license (see LICENSE) 5 | 6 | # On mip-installed host: 7 | # import sched.sun_moon_test 8 | # On PC in astronomy directory: 9 | # import sun_moon_test 10 | 11 | try: 12 | from .sun_moon import RiSet 13 | except ImportError: # Running on PC in astronomy directory 14 | from sun_moon import RiSet 15 | import time 16 | 17 | 18 | def mtime(h, m, t=None): 19 | if t is None: 20 | t = round(time.time()) 21 | tm = (t // 86400) * 86400 + h * 3600 + m * 60 22 | print(time.gmtime(tm)) 23 | return tm 24 | 25 | 26 | nresults = [] # Times in seconds from local midnight 27 | 28 | 29 | def show(rs): 30 | print(f"Sun rise {rs.sunrise(3)} set {rs.sunset(3)}") 31 | print(f"Moon rise {rs.moonrise(3)} set {rs.moonset(3)}") 32 | nresults.extend([rs.sunrise(), rs.sunset(), rs.moonrise(), rs.moonset()]) 33 | print() 34 | 35 | 36 | print("4th Dec 2023: Seattle UTC-8") 37 | rs = RiSet(lat=47.61, long=-122.35, lto=-8) # Seattle 47°36′35″N 122°19′59″W 38 | RiSet.set_time(19695 * 86400) 39 | rs.set_day() 40 | show(rs) 41 | 42 | print("4th Dec 2023: Sydney UTC+11") 43 | rs = RiSet(lat=-33.86, long=151.21, lto=11) # Sydney 33°52′04″S 151°12′36″E 44 | RiSet.set_time(19695 * 86400) 45 | rs.set_day() 46 | show(rs) 47 | 48 | print("From 4th Dec 2023: UK, UTC") 49 | rs = RiSet() 50 | for day in range(7): 51 | RiSet.set_time(19695 * 86400) 52 | rs.set_day(day) 53 | # rs.set_day(abs_to_rel_days(19695 + day)) # Start 4th Dec 2023 54 | print(f"Day: {day}") 55 | show(rs) 56 | 57 | 58 | print("4th Dec 2023: Sydney UTC+11 - test DST") 59 | # Sydney 33°52′04″S 151°12′36″E 60 | rs = RiSet(lat=-33.86, long=151.21, lto=11, dst=lambda x: x + 3600) 61 | RiSet.set_time(19695 * 86400 + 86400 / 2) 62 | rs.set_day() 63 | # rs.set_day(abs_to_rel_days(19695)) # 4th Dec 2023 64 | show(rs) 65 | 66 | 67 | # Expected results as computed on Unix build (64-bit FPU) 68 | exp = [ 69 | 27628, 70 | 58714, 71 | 85091, 72 | 46417, 73 | 20212, 74 | 71598, 75 | 2747, 76 | 41257, 77 | 29049, 78 | 57158, 79 | 82965, 80 | 46892, 81 | 29130, 82 | 57126, 83 | None, 84 | 47460, 85 | 29209, 86 | 57097, 87 | 892, 88 | 47958, 89 | 29285, 90 | 57072, 91 | 5244, 92 | 48441, 93 | 29359, 94 | 57051, 95 | 9625, 96 | 48960, 97 | 29430, 98 | 57033, 99 | 14228, 100 | 49577, 101 | 29499, 102 | 57019, 103 | 19082, 104 | 50384, 105 | 20212 + 3600, 106 | 71598 + 3600, 107 | 2747 + 3600, 108 | 41257 + 3600, 109 | ] 110 | print() 111 | max_error = 0 112 | for act, requirement in zip(nresults, exp): 113 | if act is not None and requirement is not None: 114 | err = abs(requirement - act) 115 | max_error = max(max_error, err) 116 | if err > 30: 117 | print(f"Error {requirement - act}") 118 | 119 | print(f"Maximum error {max_error}. Expect 0 on 64-bit platform, 30s on 32-bit") 120 | 121 | # Times from timeanddate.com 122 | # Seattle 123 | # Sunrise 7:40 sunset 16:18 Moonrise 23:37 Moonset 12:53 124 | # Sydney 125 | # Sunrise 5:37 sunset 19:53 Moonrise 00:45 Moonset 11:28 126 | # UK 127 | # Sunrise 8:04 sunset 15:52 Moonrise 23:02 Moonset 13:01 128 | 129 | 130 | def testup(t): # Time in secs since machine epoch 131 | t = round((t // 86400) * 86400 + 60) # 1 minute past midnight 132 | rs = RiSet() 133 | RiSet.set_time(t) 134 | rs.set_day() 135 | tr = rs.moonrise() 136 | ts = rs.moonset() 137 | print(f"testup rise {rs.moonrise(2)} set {rs.moonset(2)}") 138 | if tr is None and ts is None: 139 | print(time.gmtime(t), "No moon events") 140 | print( 141 | f"Is up {rs.is_up(False)} Has risen {rs.has_risen(False)} has set {rs.has_set(False)}" 142 | ) 143 | return 144 | # Initial state: not risen or set 145 | assert not rs.has_set(False) 146 | assert not rs.has_risen(False) 147 | if tr is not None and (ts is None or ts > tr): 148 | assert not rs.is_up(False) 149 | rs.set_time(t + tr) 150 | assert rs.has_risen(False) 151 | assert rs.is_up(False) 152 | assert not rs.has_set(False) 153 | if ts is not None: 154 | rs.set_time(t + ts) 155 | assert rs.has_risen(False) 156 | assert not rs.is_up(False) 157 | assert rs.has_set(False) 158 | return 159 | if ts is not None: 160 | assert rs.is_up(False) 161 | rs.set_time(t + ts) 162 | assert not rs.has_risen(False) 163 | assert not rs.is_up(False) 164 | assert rs.has_set(False) 165 | if tr is not None: 166 | rs.set_time(t + tr) 167 | assert rs.has_risen(False) 168 | assert rs.is_up(False) 169 | assert rs.has_set(False) 170 | return 171 | print(f"Untested state tr {tr} ts {ts}") 172 | 173 | 174 | # t = time.time() 175 | # for d in range(365): 176 | # testup(t + d * 86400) 177 | -------------------------------------------------------------------------------- /bitmap/bitmap.py: -------------------------------------------------------------------------------- 1 | # bitmap.py 2 | # Ideas for non-allocating classes based around a bitmap. Three classes are 3 | # presented: 4 | # IntSet: this is a set which is constrained to accept only integers in 5 | # a range 0 <= i <= maxval. Provides a minimal set of methods which can readily 6 | # be expanded. 7 | # SetByte: an even more minimal version of IntSet where the range is 8 | # 0 <= i <=255. 9 | # BoolList: A list of booleans. The list index is constrained to lie in range 10 | # 0 <= i <= maxval. 11 | 12 | class BitMap: 13 | def __init__(self, maxval): 14 | d, m = divmod(maxval, 8) 15 | self._ba = bytearray(d + int(m > 0)) 16 | self._size = maxval 17 | 18 | def _check(self, i): 19 | if i < 0 or i >= self._size: 20 | raise ValueError('Index out of range') 21 | 22 | def _val(self, i): 23 | self._check(i) 24 | return (self._ba[i >> 3] & 1 << (i & 7)) > 0 25 | 26 | def _set(self, i): 27 | self._check(i) 28 | self._ba[i >> 3] |= 1 << (i & 7) 29 | 30 | def _clear(self, i): 31 | self._check(i) 32 | self._ba[i >> 3] &= ~(1 << (i &7)) 33 | 34 | # Iterate through an IntSet returning members 35 | # Iterate through an IntList returning index value of True members 36 | def __iter__(self): 37 | for i in range(self._size): 38 | if self._val(i): 39 | yield i 40 | 41 | # if MyIntSet: True unless set is empty 42 | # if MyBoolList: True if any element is True 43 | def __bool__(self): 44 | for x in self._ba: 45 | if x: 46 | return True 47 | return False 48 | 49 | 50 | class IntSet(BitMap): 51 | def __init__(self, maxval=256): 52 | super().__init__(maxval) 53 | 54 | # if n in MyIntSet: 55 | def __contains__(self, i): 56 | self._check(i) 57 | return self._val(i) 58 | 59 | # MyIntSet.discard(n) 60 | def discard(self, i): 61 | self._clear(i) 62 | 63 | # MyIntSet.remove(n) 64 | def remove(self, i): 65 | if i in self: 66 | self.discard(i) 67 | else: 68 | raise KeyError(i) 69 | 70 | # MyIntSet.add(n) 71 | def add(self, i): 72 | self._set(i) 73 | 74 | # Generator iterates through set intersection. Avoids allocation. 75 | def intersec(self, other): 76 | for i in other: 77 | if i in self: 78 | yield i 79 | 80 | class BoolList(BitMap): 81 | def __init__(self, maxval=256): 82 | super().__init__(maxval) 83 | 84 | # MyBoolList[n] = True 85 | def __setitem__(self, bit, val): 86 | self._set(bit) if val else self._clear(bit) 87 | 88 | # if MyBoolList[n]: 89 | def __getitem__(self, bit): 90 | return self._val(bit) 91 | 92 | # if False in MyBoolList: 93 | def __contains__(self, i): 94 | if i: 95 | for b in self._ba: 96 | if b: 97 | return True 98 | else: 99 | for b in self._ba: 100 | if not b: 101 | return True 102 | return False 103 | 104 | 105 | # Minimal implementation of set for 0-255 106 | class SetByte: 107 | def __init__(self): 108 | self._ba = bytearray(32) 109 | 110 | def __bool__(self): 111 | for x in self._ba: 112 | if x: 113 | return True 114 | return False 115 | 116 | def __contains__(self, i): 117 | return (self._ba[i >> 3] & 1 << (i & 7)) > 0 118 | 119 | def discard(self, i): 120 | self._ba[i >> 3] &= ~(1 << (i &7)) 121 | 122 | def add(self, i): 123 | self._ba[i >> 3] |= 1 << (i & 7) 124 | 125 | # Test for set intersection 126 | bs = IntSet() 127 | bs.add(1) 128 | bs.add(2) 129 | 1 in bs 130 | c = IntSet() 131 | c.add(1) 132 | c.add(4) 133 | c.add(5) 134 | g = bs.intersec(c) 135 | next(g) 136 | next(g) 137 | 138 | -------------------------------------------------------------------------------- /buildcheck/buildcheck.py: -------------------------------------------------------------------------------- 1 | # Raise an exception if a firmware build is earlier than a given date 2 | # buildcheck((2015,10,9)) 3 | def buildcheck(tupTarget): 4 | fail = True 5 | if 'uname' in dir(os): 6 | datestring = os.uname()[3] 7 | date = datestring.split(' on')[1] 8 | idate = tuple([int(x) for x in date.split('-')]) 9 | fail = idate < tupTarget 10 | if fail: 11 | raise OSError('This driver requires a firmware build dated {:4d}-{:02d}-{:02d} or later'.format(*tupTarget)) 12 | 13 | -------------------------------------------------------------------------------- /data_to_py/data_to_py.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2016 Peter Hinch 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | import argparse 27 | import sys 28 | import os 29 | 30 | # UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE 31 | 32 | # ByteWriter takes as input a variable name and data values and writes 33 | # Python source to an output stream of the form 34 | # my_variable = b'\x01\x02\x03\x04\x05\x06\x07\x08'\ 35 | 36 | # Lines are broken with \ for readability. 37 | 38 | 39 | class ByteWriter(object): 40 | bytes_per_line = 16 41 | 42 | def __init__(self, stream, varname): 43 | self.stream = stream 44 | self.stream.write('{} =\\\n'.format(varname)) 45 | self.bytecount = 0 # For line breaks 46 | 47 | def _eol(self): 48 | self.stream.write("'\\\n") 49 | 50 | def _eot(self): 51 | self.stream.write("'\n") 52 | 53 | def _bol(self): 54 | self.stream.write("b'") 55 | 56 | # Output a single byte 57 | def obyte(self, data): 58 | if not self.bytecount: 59 | self._bol() 60 | self.stream.write('\\x{:02x}'.format(data)) 61 | self.bytecount += 1 62 | self.bytecount %= self.bytes_per_line 63 | if not self.bytecount: 64 | self._eol() 65 | 66 | # Output from a sequence 67 | def odata(self, bytelist): 68 | for byt in bytelist: 69 | self.obyte(byt) 70 | 71 | # ensure a correct final line 72 | def eot(self): # User force EOL if one hasn't occurred 73 | if self.bytecount: 74 | self._eot() 75 | self.stream.write('\n') 76 | 77 | 78 | # PYTHON FILE WRITING 79 | 80 | STR01 = """# Code generated by data_to_py.py. 81 | version = '0.1' 82 | """ 83 | 84 | STR02 = """_mvdata = memoryview(_data) 85 | 86 | def data(): 87 | return _mvdata 88 | 89 | """ 90 | 91 | def write_func(stream, name, arg): 92 | stream.write('def {}():\n return {}\n\n'.format(name, arg)) 93 | 94 | 95 | def write_data(op_path, ip_path): 96 | try: 97 | with open(ip_path, 'rb') as ip_stream: 98 | try: 99 | with open(op_path, 'w') as op_stream: 100 | write_stream(ip_stream, op_stream) 101 | except OSError: 102 | print("Can't open", op_path, 'for writing') 103 | return False 104 | except OSError: 105 | print("Can't open", ip_path) 106 | return False 107 | return True 108 | 109 | 110 | def write_stream(ip_stream, op_stream): 111 | op_stream.write(STR01) 112 | op_stream.write('\n') 113 | data = ip_stream.read() 114 | bw_data = ByteWriter(op_stream, '_data') 115 | bw_data.odata(data) 116 | bw_data.eot() 117 | op_stream.write(STR02) 118 | 119 | 120 | # PARSE COMMAND LINE ARGUMENTS 121 | 122 | def quit(msg): 123 | print(msg) 124 | sys.exit(1) 125 | 126 | DESC = """data_to_py.py 127 | Utility to convert an arbitrary binary file to Python source. 128 | Sample usage: 129 | data_to_py.py image.jpg image.py 130 | 131 | """ 132 | 133 | if __name__ == "__main__": 134 | parser = argparse.ArgumentParser(__file__, description=DESC, 135 | formatter_class=argparse.RawDescriptionHelpFormatter) 136 | parser.add_argument('infile', type=str, help='Input file path') 137 | parser.add_argument('outfile', type=str, 138 | help='Path and name of output file. Must have .py extension.') 139 | 140 | 141 | args = parser.parse_args() 142 | 143 | if not os.path.isfile(args.infile): 144 | quit("Data filename does not exist") 145 | 146 | if not os.path.splitext(args.outfile)[1].upper() == '.PY': 147 | quit('Output filename must have a .py extension.') 148 | 149 | print('Writing Python file.') 150 | if not write_data(args.outfile, args.infile): 151 | sys.exit(1) 152 | 153 | print(args.outfile, 'written successfully.') 154 | -------------------------------------------------------------------------------- /date/DATE.md: -------------------------------------------------------------------------------- 1 | # Simple Date classes 2 | 3 | The official [datetime module](https://github.com/micropython/micropython-lib/tree/master/python-stdlib/datetime) 4 | is fully featured but substantial. This `Date` class has no concept of time, 5 | but is very compact. Dates are stored as a small int. Contrary to normal MP 6 | practice, properties are used. This allows basic arithmetic syntax while 7 | ensuring automatic rollover. The speed penalty of properties is unlikely to be 8 | a factor in date operations. 9 | 10 | The `Date` class provides basic arithmetic and comparison methods. The 11 | `DateCal` subclass adds pretty printing and methods to assist in creating 12 | calendars. 13 | 14 | [Return to main readme](../README.md) 15 | 16 | # Date class 17 | 18 | The `Date` class embodies a single date value which may be modified, copied 19 | and compared with other `Date` instances. 20 | 21 | ## Constructor 22 | 23 | This takes a single optional arg: 24 | * `lt=None` By default the date is initialised from system time. To set the 25 | date from another time source, a valid 26 | [localtime/gmtime](http://docs.micropython.org/en/latest/library/time.html#time.localtime) 27 | tuple may be passed. 28 | 29 | ## Method 30 | 31 | * `now` Arg `lt=None`. Sets the instance to the current date, from system time 32 | or `lt` as described above. 33 | 34 | ## Writeable properties 35 | 36 | * `year` e.g. 2023. 37 | * `month` 1 == January. May be set to any number, years will roll over if 38 | necessary. e.g. `d.month += 15` or `d.month -= 1`. 39 | * `mday` Adjust day in current month. Allowed range `1..month_length`. 40 | * `day` Days since epoch. Note that the epoch varies with platform - the value 41 | may be treated as an opaque small integer. Use to adjust a date with rollover 42 | (`d.day += 7`) or to assign one date to another (`date2.day = date1.day`). May 43 | also be used to represnt a date as a small int for saving to a file. 44 | 45 | ## Read-only property 46 | 47 | * `wday` Day of week. 0==Monday 6==Sunday. 48 | 49 | ## Date comparisons 50 | 51 | Python "magic methods" enable date comparisons using standard operators `<`, 52 | `<=`, `>`, `>=`, `==`, `!=`. 53 | 54 | # DateCal class 55 | 56 | This adds pretty formatting and functionality to return additional information 57 | about the current date. The added methods and properties do not change the 58 | date value. Primarily intended for calendars. 59 | 60 | ## Constructor 61 | 62 | This takes a single optional arg: 63 | * `lt=None` See `Date` constructor. 64 | 65 | ## Methods 66 | 67 | * `time_offset` arg `hr=6`. This returns 0 or 1, being the offset in hours of 68 | UK local time to UTC. By default the change occurs when the date changes at 69 | 00:00 UTC on the last Sunday in March and October. If an hour value is passed, 70 | the change will occur at the correct 01:00 UTC. The value of `hr` may be an 71 | `int` or a `float`. This method will need to be adapted for other geographic 72 | locations. See [note below](./DATE.md#DST). 73 | * `wday_n` arg `mday=1`. Return the weekday for a given day of the month. 74 | * `mday_list` arg `wday`. Given a weekday, for the current month return an 75 | ordered list of month days matching that weekday. 76 | 77 | ## Read-only properties 78 | 79 | * `month_length` Length of month in days. 80 | * `day_str` Day of week as a string, e.g. "Wednesday". 81 | * `month_str` Month as a string, e.g. "August". 82 | 83 | ## Class variables 84 | 85 | * `days` A 7-tuple `("Monday", "Tuesday"...)` 86 | * `months` A 12-tuple `("January", "February",...)` 87 | 88 | # Example usage 89 | 90 | The following code fragments illustrate typical usage: 91 | ```python 92 | from date import Date 93 | d = Date() 94 | d.month = 1 # Set to January 95 | d.month -= 2 # Date changes to same mday in November previous year. 96 | d.mday = 25 # Set absolute day of month 97 | d.day += 7 # Advance date by one week. Month/year rollover is handled. 98 | today = Date() 99 | if d == today: # Date comparisons 100 | print("Today") # do something 101 | new_date = Date() 102 | new_date.day = d.day # Assign d to new_date: now new_date == d. 103 | print(d) # Basic numeric print. 104 | ``` 105 | The DateCal class: 106 | ```python 107 | from date import DateCal 108 | d = DateCal() 109 | # Given a system running UTC, enable a display of local time (UK example) 110 | d.now() 111 | t = time.gmtime() # System time, assumed to be UTC 112 | hour_utc = t[3] + t[4]/60 + t[5]/3600 # Hour with fractional part 113 | hour = (t[3] + d.time_offset(hour_utc)) % 24 114 | print(f"Local time {hour:02d}:{t[4]:02d}:{t[5]:02d}") 115 | print(d) # Pretty print 116 | x = d.wday_n(1) # Get day of week of 1st day of month 117 | sundays = d.mday_list(6) # List Sundays for the month. 118 | wday_last = d.wday_n(d.month_length) # Weekday of last day of month 119 | ``` 120 | ## DST 121 | 122 | Common microcontroller practice is for system time to be permanently set to UTC 123 | or local winter time. This avoids sudden changes in system time which can 124 | disrupt continuously running applications. Where local time is required the 125 | `time_offset` method accepts the current UTC hours value (with fractional part) 126 | and returns an offset measured in hours. This may be used to display local time. 127 | 128 | The principal purpose of this module is to provide a lightweight `Date` class. 129 | Time support is rudimentary, with the `time_offset` method illustrating a 130 | minimal way to provide a screen-based calendar with a clock display. For 131 | applications requiring full featured time support, see the official 132 | [datetime module](https://github.com/micropython/micropython-lib/tree/master/python-stdlib/datetime). Also 133 | [this scheduler](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md) 134 | which enables `cron`-like scheduling of future events. 135 | -------------------------------------------------------------------------------- /date/date.py: -------------------------------------------------------------------------------- 1 | # date.py Minimal Date class for micropython 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023-2024 Peter Hinch 5 | 6 | from time import mktime, localtime 7 | from sys import implementation 8 | if implementation.name != "micropython": 9 | const = lambda x : x 10 | 11 | _SECS_PER_DAY = const(86400) 12 | def leap(year): 13 | return bool((not year % 4) ^ (not year % 100)) 14 | 15 | class Date: 16 | 17 | def __init__(self, lt=None): 18 | self.callback = lambda : None # No callback until set 19 | self.now(lt) 20 | 21 | def now(self, lt=None): 22 | self._lt = list(localtime()) if lt is None else list(lt) 23 | self._update() 24 | 25 | def _update(self, ltmod=True): # If ltmod is False ._cur has been changed 26 | if ltmod: # Otherwise ._lt has been modified 27 | self._lt[3] = 6 28 | self._cur = mktime(tuple(self._lt)) // _SECS_PER_DAY 29 | self._lt = list(localtime(self._cur * _SECS_PER_DAY)) 30 | self.callback() 31 | 32 | def _mlen(self, d=bytearray((31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31))): 33 | days = d[self._lt[1] - 1] 34 | return days if days else (29 if leap(self._lt[0]) else 28) 35 | 36 | @property 37 | def year(self): 38 | return self._lt[0] 39 | 40 | @year.setter 41 | def year(self, v): 42 | if self.mday == 29 and self.month == 2 and not leap(v): 43 | self.mday = 28 # Ensure it doesn't skip a month 44 | self._lt[0] = v 45 | self._update() 46 | 47 | @property 48 | def month(self): 49 | return self._lt[1] 50 | 51 | # Can write d.month = 4 or d.month += 15 52 | @month.setter 53 | def month(self, v): 54 | y, m = divmod(v - 1, 12) 55 | self._lt[0] += y 56 | self._lt[1] = m + 1 57 | self._lt[2] = min(self._lt[2], self._mlen()) 58 | self._update() 59 | 60 | @property 61 | def mday(self): 62 | return self._lt[2] 63 | 64 | @mday.setter 65 | def mday(self, v): 66 | if not 0 < v <= self._mlen(): 67 | raise ValueError(f"mday {v} is out of range") 68 | self._lt[2] = v 69 | self._update() 70 | 71 | @property 72 | def day(self): # Days since epoch. 73 | return self._cur 74 | 75 | @day.setter 76 | def day(self, v): # Usage: d.day += 7 or date_1.day = d.day. 77 | self._cur = v 78 | self._update(False) # Flag _cur change 79 | 80 | # Read-only properties 81 | 82 | @property 83 | def wday(self): 84 | return self._lt[6] 85 | 86 | # Date comparisons 87 | 88 | def __lt__(self, other): 89 | return self.day < other.day 90 | 91 | def __le__(self, other): 92 | return self.day <= other.day 93 | 94 | def __eq__(self, other): 95 | return self.day == other.day 96 | 97 | def __ne__(self, other): 98 | return self.day != other.day 99 | 100 | def __gt__(self, other): 101 | return self.day > other.day 102 | 103 | def __ge__(self, other): 104 | return self.day >= other.day 105 | 106 | def __str__(self): 107 | return f"{self.year}/{self.month}/{self.mday}" 108 | 109 | 110 | class DateCal(Date): 111 | days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") 112 | months = ( 113 | "January", 114 | "February", 115 | "March", 116 | "April", 117 | "May", 118 | "June", 119 | "July", 120 | "August", 121 | "September", 122 | "October", 123 | "November", 124 | "December", 125 | ) 126 | 127 | def __init__(self, lt=None): 128 | super().__init__(lt) 129 | 130 | @property 131 | def month_length(self): 132 | return self._mlen() 133 | 134 | @property 135 | def day_str(self): 136 | return self.days[self.wday] 137 | 138 | @property 139 | def month_str(self): 140 | return self.months[self.month - 1] 141 | 142 | def wday_n(self, mday=1): 143 | return (self._lt[6] - self._lt[2] + mday) % 7 144 | 145 | def mday_list(self, wday): 146 | ml = self._mlen() # 1 + ((wday - wday1) % 7) 147 | d0 = 1 + ((wday - (self._lt[6] - self._lt[2] + 1)) % 7) 148 | return [d for d in range(d0, ml + 1, 7)] 149 | 150 | # Optional: return UK DST offset in hours. Can pass hr to ensure that time change occurs 151 | # at 1am UTC otherwise it occurs on date change (0:0 UTC) 152 | # offs is offset by month 153 | def time_offset(self, hr=6, offs=bytearray((0, 0, 3, 1, 1, 1, 1, 1, 1, 10, 0, 0))): 154 | ml = self._mlen() 155 | wdayld = self.wday_n(ml) # Weekday of last day of month 156 | mday_sun = self.mday_list(6)[-1] # Month day of last Sunday 157 | m = offs[self._lt[1] - 1] 158 | if m < 3: 159 | return m # Deduce time offset from month alone 160 | return int( 161 | ((self._lt[2] < mday_sun) or (self._lt[2] == mday_sun and hr <= 1)) ^ (m == 3) 162 | ) # Months where offset changes 163 | 164 | def __str__(self): 165 | return f"{self.day_str} {self.mday} {self.month_str} {self.year}" 166 | -------------------------------------------------------------------------------- /encoders/encoder.py: -------------------------------------------------------------------------------- 1 | # encoder.py 2 | 3 | # Copyright (c) 2016-2021 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import pyb 7 | 8 | class Encoder: 9 | def __init__(self, pin_x, pin_y, reverse, scale): 10 | self.reverse = reverse 11 | self.scale = scale 12 | self.forward = True 13 | self.pin_x = pin_x 14 | self.pin_y = pin_y 15 | self._pos = 0 16 | self.x_interrupt = pyb.ExtInt(pin_x, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.x_callback) 17 | self.y_interrupt = pyb.ExtInt(pin_y, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.y_callback) 18 | 19 | def x_callback(self, line): 20 | self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse 21 | self._pos += 1 if self.forward else -1 22 | 23 | def y_callback(self, line): 24 | self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse ^ 1 25 | self._pos += 1 if self.forward else -1 26 | 27 | def position(self, value=None): 28 | if value is not None: 29 | self._pos = round(value / self.scale) 30 | return self._pos * self.scale 31 | 32 | def reset(self): 33 | self._pos = 0 34 | 35 | def value(self, value=None): 36 | if value is not None: 37 | self._pos = value 38 | return self._pos 39 | -------------------------------------------------------------------------------- /encoders/encoder_conditioner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/encoders/encoder_conditioner.png -------------------------------------------------------------------------------- /encoders/encoder_conditioner_digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/encoders/encoder_conditioner_digital.png -------------------------------------------------------------------------------- /encoders/encoder_portable.py: -------------------------------------------------------------------------------- 1 | # encoder_portable.py 2 | 3 | # Encoder Support: this version should be portable between MicroPython platforms 4 | # Thanks to Evan Widloski for the adaptation to use the machine module 5 | 6 | # Copyright (c) 2017-2022 Peter Hinch 7 | # Released under the MIT License (MIT) - see LICENSE file 8 | 9 | from machine import Pin 10 | 11 | class Encoder: 12 | def __init__(self, pin_x, pin_y, scale=1): 13 | self.scale = scale 14 | self.forward = True 15 | self.pin_x = pin_x 16 | self.pin_y = pin_y 17 | self._x = pin_x() 18 | self._y = pin_y() 19 | self._pos = 0 20 | try: 21 | self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback, hard=True) 22 | self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback, hard=True) 23 | except TypeError: 24 | self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback) 25 | self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback) 26 | 27 | def x_callback(self, pin_x): 28 | if (x := pin_x()) != self._x: # Reject short pulses 29 | self._x = x 30 | self.forward = x ^ self.pin_y() 31 | self._pos += 1 if self.forward else -1 32 | 33 | def y_callback(self, pin_y): 34 | if (y := pin_y()) != self._y: 35 | self._y = y 36 | self.forward = y ^ self.pin_x() ^ 1 37 | self._pos += 1 if self.forward else -1 38 | 39 | def position(self, value=None): 40 | if value is not None: 41 | self._pos = round(value / self.scale) # Improvement provided by @IhorNehrutsa 42 | return self._pos * self.scale 43 | 44 | def value(self, value=None): 45 | if value is not None: 46 | self._pos = value 47 | return self._pos 48 | -------------------------------------------------------------------------------- /encoders/encoder_rp2.py: -------------------------------------------------------------------------------- 1 | # encoder_rp2.py Uses the PIO for rapid response on RP2 chips (Pico) 2 | 3 | # Copyright (c) 2022 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | # PIO and SM code written by Sandor Attila Gerendi (@sanyi) 7 | # https://github.com/micropython/micropython/pull/6894 8 | 9 | from machine import Pin 10 | from array import array 11 | import rp2 12 | 13 | # Test with encoder on pins 2 and 3: 14 | # e = Encoder(0, Pin(2)) 15 | 16 | # while True: 17 | # time.sleep(1) 18 | # print(e.value()) 19 | 20 | # Closure enables Viper to retain state. Currently (V1.17) nonlocal doesn't 21 | # work: https://github.com/micropython/micropython/issues/8086 22 | # so using arrays. 23 | def make_isr(pos): 24 | old_x = array("i", (0,)) 25 | 26 | @micropython.viper 27 | def isr(sm): 28 | i = ptr32(pos) 29 | p = ptr32(old_x) 30 | while sm.rx_fifo(): 31 | v: int = int(sm.get()) & 3 32 | x: int = v & 1 33 | y: int = v >> 1 34 | s: int = 1 if (x ^ y) else -1 35 | i[0] = i[0] + (s if (x ^ p[0]) else (0 - s)) 36 | p[0] = x 37 | 38 | return isr 39 | 40 | 41 | # Args: 42 | # StateMachine no. (0-7): each instance must have a different sm_no. 43 | # An initialised input Pin: this and the next pin are the encoder interface. 44 | # Pins must have pullups (internal or, preferably, low value 1KΩ to 3.3V). 45 | class Encoder: 46 | def __init__(self, sm_no, base_pin, scale=1): 47 | self.scale = scale 48 | self._pos = array("i", (0,)) # [pos] 49 | self.sm = rp2.StateMachine(sm_no, self.pio_quadrature, in_base=base_pin) 50 | self.sm.irq(make_isr(self._pos)) # Instantiate the closure 51 | self.sm.exec("set(y, 99)") # Initialise y: guarantee different to the input 52 | self.sm.active(1) 53 | 54 | @rp2.asm_pio() 55 | def pio_quadrature(in_init=rp2.PIO.IN_LOW): 56 | wrap_target() 57 | label("again") 58 | in_(pins, 2) 59 | mov(x, isr) 60 | jmp(x_not_y, "push_data") 61 | mov(isr, null) 62 | jmp("again") 63 | label("push_data") 64 | push() 65 | irq(block, rel(0)) 66 | mov(y, x) 67 | wrap() 68 | 69 | def position(self, value=None): 70 | if value is not None: 71 | self._pos[0] = round(value / self.scale) 72 | return self._pos[0] * self.scale 73 | 74 | def value(self, value=None): 75 | if value is not None: 76 | self._pos[0] = value 77 | return self._pos[0] 78 | -------------------------------------------------------------------------------- /encoders/encoder_timed.py: -------------------------------------------------------------------------------- 1 | # encoder_timed.py 2 | 3 | # Copyright (c) 2016-2021 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | # Improvements provided by IhorNehrutsa 6 | 7 | import utime 8 | from machine import Pin, disable_irq, enable_irq 9 | 10 | class EncoderTimed: 11 | def __init__(self, pin_x, pin_y, scale=1): 12 | self.scale = scale # Optionally scale encoder rate to distance/angle 13 | self.tprev = -1 14 | self.tlast = -1 15 | self.forward = True 16 | self.pin_x = pin_x 17 | self.pin_y = pin_y 18 | self._pos = 0 19 | try: 20 | self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback, hard=True) 21 | self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback, hard=True) 22 | except TypeError: 23 | self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback) 24 | self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback) 25 | 26 | def x_callback(self, line): 27 | self.forward = self.pin_x.value() ^ self.pin_y.value() 28 | self._pos += 1 if self.forward else -1 29 | self.tprev = self.tlast 30 | self.tlast = utime.ticks_us() 31 | 32 | def y_callback(self, line): 33 | self.forward = self.pin_x.value() ^ self.pin_y.value() ^ 1 34 | self._pos += 1 if self.forward else -1 35 | self.tprev = self.tlast 36 | self.tlast = utime.ticks_us() 37 | 38 | def rate(self): # Return rate in signed distance/angle per second 39 | state = disable_irq() 40 | tlast = self.tlast # Cache current values 41 | tprev = self.tprev 42 | enable_irq(state) 43 | if self.tprev == -1: # No valid times yet 44 | return 0.0 45 | dt = utime.ticks_diff(utime.ticks_us(), tlast) 46 | if dt > 2_000_000: # Stopped 47 | return 0.0 48 | dt = utime.ticks_diff(tlast, tprev) 49 | if dt == 0: # Could happen on future rapid hardware 50 | return 0.0 51 | result = self.scale * 1_000_000.0/dt 52 | return result if self.forward else -result 53 | 54 | def position(self, value=None): 55 | if value is not None: 56 | self._pos = round(value / self.scale) 57 | return self._pos * self.scale 58 | 59 | def reset(self): 60 | self._pos = 0 61 | 62 | def value(self, value=None): 63 | if value is not None: 64 | self._pos = value 65 | return self._pos 66 | -------------------------------------------------------------------------------- /encoders/quadrature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/encoders/quadrature.jpg -------------------------------------------------------------------------------- /encoders/state_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/encoders/state_diagram.png -------------------------------------------------------------------------------- /encoders/synchroniser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/encoders/synchroniser.png -------------------------------------------------------------------------------- /fastbuild/47-ftdi.rules: -------------------------------------------------------------------------------- 1 | # FTDI lead 2 | ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ENV{MTP_NO_PROBE}="1" 3 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", MODE:="0666" 4 | KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", SYMLINK+="ftdi", MODE:="0666" 5 | 6 | -------------------------------------------------------------------------------- /fastbuild/48-wipy.rules: -------------------------------------------------------------------------------- 1 | # 0403:6015 - WiPy board ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", ENV{ID_MM_DEVICE_IGNORE}="1" 2 | # WiPy appears as /dev/wipy 3 | ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", ENV{MTP_NO_PROBE}="1" 4 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", MODE:="0666" 5 | KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", SYMLINK+="wipy", MODE:="0666" 6 | 7 | -------------------------------------------------------------------------------- /fastbuild/49-micropython.rules: -------------------------------------------------------------------------------- 1 | # f055:9800, 9801, 9802 MicroPython pyboard 2 | # Instantiate Pyboard V1.0, V1.1 or Lite as /dev/pyoard 3 | ATTRS{idVendor}=="f055", ENV{MTP_NO_PROBE}="1" 4 | ATTRS{idVendor}=="f055", ENV{ID_MM_DEVICE_IGNORE}="1" 5 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="f055", MODE:="0666" 6 | KERNEL=="ttyACM*", ATTRS{idVendor}=="f055", SYMLINK+="pyboard", MODE:="0666" 7 | KERNEL=="ttyUSB*", ATTRS{idVendor}=="f055", SYMLINK+="pyboard", MODE:="0666" 8 | # DFU mode 9 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE:="0666" 10 | -------------------------------------------------------------------------------- /fastbuild/README.md: -------------------------------------------------------------------------------- 1 | # fastbuild - Pull and build Pyboard firmware under Linux 2 | 3 | The following is largely obsolete. Please see [mpbuild](https://github.com/mattytrentini/mpbuild) 4 | for a modern solution using containers. 5 | 6 | These scripts are intended to speed and simplify rebuilding firmware from 7 | source notably where pyboards of different types are in use, or when 8 | frozen bytecode necessitates repeated compilation and deployment. In 9 | particular `buildpyb` will detect the attached Pyboard type, build the 10 | appropriate firmware, put the board into DFU mode and deploy it. 11 | 12 | The scripts should be run as your normal user and can proceed without user 13 | interaction. 14 | 15 | Includes udev rules to avoid jumps from `/dev/ttyACM0` to `/dev/ttyACM1` 16 | and ensuring Pyboards of all types appear as `/dev/pyboard`. Rules are also 17 | offered for USB connected WiPy (V1.0) and FTDI USB/serial adaptors. 18 | 19 | These scripts use Python scripts `pyb_boot` to put the Pyboard into DFU mode 20 | and `pyb_check` to determine the type of attached board. These use the 21 | `pyboard.py` module in the source tree to execute scripts on the attached 22 | board. 23 | 24 | The scripts will require minor edits to reflect your directory structure. 25 | 26 | Scripts updated 12 Sep 2021 to fix handling of submodules. 27 | 28 | ###### [Main README](../README.md) 29 | 30 | # Frozen modules and manifests 31 | 32 | The method of specifying modules to be frozen has changed (as of Oct 2019). 33 | The files and directories to be frozen are now specified in a file with the 34 | default name `manifest.py`. This may be found in `/ports/stm32/boards` or the 35 | eqivalent for other ports. 36 | 37 | In practice it can be advantageous to override the default. You might want to 38 | freeze a different set of files depending on the specific board or project. 39 | This is done by issuing 40 | ``` 41 | make BOARD=$BOARD FROZEN_MANIFEST=$MANIFEST 42 | ``` 43 | where `BOARD` specifies the target (e.g. 'PYBV11') and `MANIFEST` specifies the 44 | path to the manifest file (e.g. '~/my_manifest.py'). 45 | 46 | A manifest file comprises `include` and `freeze` statements. The latter have 47 | one or two args. The first is a directory specifier. If the second exists it 48 | can specify a single file or more, by passing an iterable. Consider the 49 | following manifest file: 50 | ```python 51 | include("$(MPY_DIR)/ports/stm32/boards/PYBD_SF2/manifest.py") 52 | freeze('$(MPY_DIR)/drivers/dht', 'dht.py') 53 | freeze('$(MPY_DIR)/tools', ('upip.py', 'upip_utarfile.py')) 54 | freeze('/path/to/pyb_d_modules') 55 | ``` 56 | Taking the lines in order: 57 | 1. This includes another manifest file located in the source tree. 58 | 2. The single file argument freezes the file 'dht.py' found in the MicroPython 59 | source tree `drivers` directory. 60 | 3. Passing an iterable causes the two specified files to be frozen. 61 | 4. Passing a directory without arguments causes all files and subdirectories 62 | to be frozen. Assume '../pyb_d_modules' contains a file `rats.py` and a 63 | subdirectory `foo` containing `bar.py`. Then `help('modules')` will show 64 | `rats` and `foo/bar`. This means that Python packages are frozen correctly. 65 | 66 | On Linux symlinks are handled as you would expect. 67 | 68 | # The build scripts 69 | 70 | ### Optional Edit (all scripts) 71 | 72 | In these scripts you may wish to edit the `-j 8` argument to `make`. This 73 | radically speeds build on a multi core PC. Empirically 8 gave the fastest build 74 | on my Core i7 4/8 core laptop: adjust to suit your PC. 75 | 76 | ### Dependencies and setup (on PC) 77 | 78 | Python3 79 | The following Bash code installs pyserial, copies `49-micropython.rules` to 80 | (on most distros) `/etc/udev/rules.d`. It installs `rshell` if you plan to 81 | use it (recommended). 82 | 83 | As root: 84 | ``` 85 | apt-get install python3-serial 86 | pip install pyserial 87 | cp 49-micropython.rules /etc/udev/rules.d 88 | pip3 install rshell 89 | ``` 90 | 91 | The build scripts expect an environment variable MPDIR holding the path to the 92 | MicroPython source tree. To set this up, as normal user issue (edited for your 93 | path to the MicroPython source tree): 94 | 95 | ``` 96 | cd ~ 97 | echo export MPDIR='/mnt/qnap2/data/Projects/MicroPython/micropython' >> .bashrc 98 | echo >> .bashrc 99 | ``` 100 | 101 | Close and restart the terminal session before proceding. 102 | 103 | Verify that `pyboard.py` works. To do this, close and restart the terminal 104 | session. Run Python3, paste the following and check that the red LED lights: 105 | 106 | ```python 107 | import os 108 | mp = os.getenv('MPDIR') 109 | sys.path.append(''.join((mp, '/tools'))) 110 | import pyboard 111 | pyb = pyboard.Pyboard('/dev/pyboard') 112 | pyb.enter_raw_repl() 113 | pyb.exec('pyb.LED(1).on()') 114 | pyb.exit_raw_repl() 115 | ``` 116 | 117 | ### Build script: `buildpyb` 118 | 119 | This checks the attached pyboard. If it's a V1.0, V1.1 or Lite it or a Pyboard 120 | D series it builds the correct firmware and deploys it. Otherwise it produces 121 | an error message. 122 | 123 | It freezes a different set of files depending on whether the board is a Pyboard 124 | V1.x or a Pyboard D. It can readily be adapted for finer-grain control or to 125 | produce project-specific builds. 126 | 127 | You will need to change the `MANIFESTS` variable which is the directory 128 | specifier for my manifest files. 129 | 130 | Optional argument `--clean` - if supplied does a `make clean` to delete 131 | all files produced by the previous build before proceeding. 132 | 133 | ### Update source: `buildnew` 134 | 135 | Report state of master branch, update sources and issue `make clean` for 136 | Pyboard variants and ESP8266. Builds cross compiler and unix port. 137 | 138 | If you don't use the Unix build you may wish to delete the unix make commands. 139 | 140 | ### ESP8266 Build 141 | 142 | `buildesp` A script to build and deploy ESP8266 firmware. Accepts optional 143 | `--clean` or `--erase` arguments. Both perform a `make clean` but the second 144 | also erases the ESP8266 flash. 145 | 146 | You will need to change the `MANIFEST` variable which is the directory 147 | specifier for my esp8266 manifest file. 148 | -------------------------------------------------------------------------------- /fastbuild/buildesp: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | cd /mnt/qnap2/data/Projects/MicroPython/micropython/ports/esp8266 3 | MANIFEST='/mnt/qnap2/Scripts/manifests/esp8266_manifest.py' 4 | 5 | if [ $# -eq 1 ] && [ $1 = "--clean" ] 6 | then 7 | make clean 8 | fi 9 | 10 | if [ $# -eq 1 ] && [ $1 = "--erase" ] 11 | then 12 | make clean 13 | if esptool.py --port /dev/ttyUSB0 erase_flash 14 | then 15 | echo Flash erased OK 16 | else 17 | echo Connection failure 18 | exit 1 19 | fi 20 | fi 21 | 22 | make submodules 23 | if make -j 8 FROZEN_MANIFEST=$MANIFEST 24 | then 25 | sleep 1 26 | esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash --flash_size=detect -fm dio 0 build-GENERIC/firmware-combined.bin 27 | cd - 28 | sleep 4 29 | rshell -p /dev/ttyUSB0 --editor nano --buffer-size=30 30 | else 31 | echo Build failure 32 | fi 33 | cd - 34 | -------------------------------------------------------------------------------- /fastbuild/buildnew: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Update MicroPython source and prepare for build 3 | 4 | cd $MPDIR 5 | echo Working... 6 | git checkout master 7 | git pull origin master --tags 8 | git submodule sync 9 | git submodule update --init 10 | cd mpy-cross 11 | make clean 12 | make -j 8 13 | cd ../ports/stm32 14 | make BOARD=PYBV11 clean 15 | make BOARD=PYBV10 clean 16 | make BOARD=PYBLITEV10 clean 17 | make BOARD=PYBD_SF2 clean 18 | make BOARD=PYBD_SF3 clean 19 | make BOARD=PYBD_SF6 clean 20 | cd ../esp8266 21 | make clean 22 | cd ../unix 23 | make clean 24 | make submodules 25 | make -j 8 26 | -------------------------------------------------------------------------------- /fastbuild/buildpyb: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Detect attached pyboard variant build and deploy 3 | # Assumes only one device attached and that this will appear as /dev/pyboard (udev rule) 4 | # requires pyb_check 5 | # Also requires the pyboard.py utility to be on the path (micropython/tools/pyboard.py) 6 | 7 | MPDEVICE='/dev/pyboard' 8 | MANIFESTS='/mnt/qnap2/Scripts/manifests' 9 | # Determine board type 10 | BOARD=$(pyb_check $MPDEVICE) 11 | # Currently have only two manifest variants for V1.x and D 12 | if [[ $BOARD == 'PYBV11' || $BOARD == 'PYBV10' || $BOARD == 'PYBLITEV10' ]] 13 | then 14 | MANIFEST=$MANIFESTS/pyb_v1_manifest.py 15 | fi 16 | if [[ $BOARD == 'PYBD_SF2' || $BOARD == 'PYBD_SF3' || $BOARD == 'PYBD_SF6' ]] 17 | then 18 | MANIFEST=$MANIFESTS/pyb_d_manifest.py 19 | fi 20 | 21 | # Check for user override of frozen directory 22 | if [ $FROZEN_DIR ] 23 | then 24 | echo Frozen modules located in $FROZEN_DIR 25 | else 26 | FROZEN_DIR='modules' 27 | fi 28 | 29 | if [ $BOARD ] 30 | then 31 | echo Building for $BOARD 32 | cd $MPDIR/ports/stm32 33 | if [ $# -eq 1 ] && [ $1 = "--minimal" ] 34 | then 35 | MANIFEST=$MANIFESTS/minimal_manifest.py 36 | make BOARD=$BOARD clean 37 | fi 38 | if [ $# -eq 1 ] && [ $1 = "--clean" ] 39 | then 40 | make BOARD=$BOARD clean 41 | fi 42 | make submodules 43 | if make -j 8 BOARD=$BOARD FROZEN_MANIFEST=$MANIFEST MICROPY_VFS_LFS2=1 && pyb_boot $MPDEVICE 44 | then 45 | sleep 1 46 | make PYTHON=python3 BOARD=$BOARD FROZEN_MANIFEST=$MANIFEST deploy 47 | else 48 | echo Build failure 49 | fi 50 | cd - 51 | else 52 | echo Incorrect board type 53 | fi 54 | 55 | -------------------------------------------------------------------------------- /fastbuild/pyb_boot: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # Called from buildpyb 4 | # Put pyboard into DFU mode. 5 | 6 | import sys 7 | import os 8 | mp = os.getenv('MPDIR') 9 | sys.path.append(''.join((mp, '/tools'))) 10 | import pyboard 11 | 12 | errmsg = 'pyb_boot error: usage pyb_boot device.' 13 | def main(): 14 | if len(sys.argv) < 2: 15 | print(errmsg) 16 | sys.exit(1) 17 | device = sys.argv[1] 18 | pyb=pyboard.Pyboard(device) 19 | pyb.enter_raw_repl() 20 | try: 21 | pyb.exec_raw('import pyb;pyb.bootloader()') 22 | print('Failed to enter DFU mode') 23 | except Exception: # It will throw one! 24 | pass 25 | 26 | if __name__ == "__main__": 27 | main() 28 | 29 | -------------------------------------------------------------------------------- /fastbuild/pyb_check: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Called from buildpyb 5 | # Arg: device (e.g. '/dev/pyboard') 6 | 7 | import sys 8 | import os, os.path 9 | mp = os.getenv('MPDIR') 10 | sys.path.append(''.join((mp, '/tools'))) 11 | import pyboard 12 | 13 | errmsg = 'Usage pyb_check device' 14 | d = {b'PYBv1.1' : 'PYBV11', b'PYBv1.0' : 'PYBV10', b'PYBLITEv1.0' : 'PYBLITEV10', 15 | b'PYBD-SF2W' : 'PYBD_SF2', b'PYBD-SF3W' : 'PYBD_SF3', b'PYBD-SF6W' : 'PYBD_SF6', 16 | b'PYBD_SF2W' : 'PYBD_SF2', b'PYBD_SF3W' : 'PYBD_SF3', b'PYBD_SF6W' : 'PYBD_SF6',} 17 | 18 | def main(): 19 | if len(sys.argv) < 2: 20 | print(errmsg, file=sys.stderr) 21 | sys.exit(1) 22 | device = sys.argv[1] 23 | if not os.path.exists(device): 24 | print('Device {} does not exist'.format(device), file=sys.stderr) 25 | sys.exit(1) 26 | pybd = pyboard.Pyboard(device) 27 | pybd.enter_raw_repl() 28 | hardware = pybd.exec('import os; print(os.uname()[4].split(' ')[0])').strip() 29 | pybd.exit_raw_repl() 30 | if hardware in d: 31 | print(d[hardware]) 32 | 33 | if __name__ == "__main__": 34 | main() 35 | 36 | -------------------------------------------------------------------------------- /functor_singleton/README.md: -------------------------------------------------------------------------------- 1 | # Singletons and Functors 2 | 3 | These closely related concepts describe classes which support only a single 4 | instance. They share a common purpose of avoiding the need for global data by 5 | providing a callable capable of retaining state and whose scope may be that of 6 | the module. 7 | 8 | In both cases implementation is via very similar class decorators. 9 | 10 | # Singleton class decorator 11 | 12 | A singleton is a class with only one instance. Some IT gurus argue against them 13 | on the grounds that project aims can change: a need for multiple instances may 14 | arise later. My view is that they have merit in defining interfaces to hardware 15 | objects. You might be quite certain that your brometer will have only one 16 | pressure sensor. 17 | 18 | The advantage of a singleton is that it removes the need for a global instance 19 | or for passing an instance between functions. The sole instance is efficiently 20 | retrieved at any point in the code using function call syntax. 21 | 22 | ```python 23 | def singleton(cls): 24 | instance = None 25 | def getinstance(*args, **kwargs): 26 | nonlocal instance 27 | if instance is None: 28 | instance = cls(*args, **kwargs) 29 | return instance 30 | return getinstance 31 | 32 | @singleton 33 | class MySingleton: 34 | def __init__(self, arg): 35 | self.state = arg 36 | print('In __init__', arg) 37 | 38 | def foo(self, arg): 39 | print('In foo', arg + self.state) 40 | 41 | ms = MySingleton(42) # prints 'In __init__ 42' 42 | x = MySingleton() # No output: assign existing instance to x 43 | x.foo(5) # prints 'In foo 47': original state + 5 44 | ``` 45 | The first call instantiates the object and sets its initial state. Subsequent 46 | calls retrieve the original object. 47 | 48 | There are other ways of achieving singletons. One is to define a (notionally 49 | private) class in a module. The module API contains an access function. There 50 | is a private instance, initially `None`. The function checks if the instance is 51 | `None`. If so it instantiates the object and assigns it to the instance. In all 52 | cases it returns the instance. 53 | 54 | Both have similar logic. The decorator avoids the need for a separate module. 55 | 56 | # Functor class decorator 57 | 58 | The term "functor" derives from "function constructor". It is a function-like 59 | object which can retain state. Like singletons the aim is to avoid globals: the 60 | state is contained in the functor instance. 61 | 62 | ```python 63 | def functor(cls): 64 | instance = None 65 | def getinstance(*args, **kwargs): 66 | nonlocal instance 67 | if instance is None: 68 | instance = cls(*args, **kwargs) 69 | return instance 70 | return instance(*args, **kwargs) 71 | return getinstance 72 | 73 | @functor 74 | class MyFunctor: 75 | def __init__(self, arg): 76 | self.state = arg 77 | print('In __init__', arg) 78 | 79 | def __call__(self, arg): 80 | print('In __call__', arg + self.state) 81 | return self 82 | 83 | MyFunctor(42) # prints 'In __init__ 42' 84 | MyFunctor(5) # 'In __call__ 47' 85 | ``` 86 | In both test cases the object returned is the functor instance. 87 | 88 | A use case is in asynchronous programming. The constructor launches a 89 | continuously running task. Subsequent calls alter the behaviour of that task. 90 | The following simple example has the task waiting for a period which can be 91 | changed at runtime: 92 | 93 | ```python 94 | import uasyncio as asyncio 95 | import pyb 96 | 97 | def functor(cls): 98 | instance = None 99 | def getinstance(*args, **kwargs): 100 | nonlocal instance 101 | if instance is None: 102 | instance = cls(*args, **kwargs) 103 | return instance 104 | return instance(*args, **kwargs) 105 | return getinstance 106 | 107 | @functor 108 | class FooFunctor: 109 | def __init__(self, led, interval): 110 | self.led = led 111 | self.interval = interval 112 | asyncio.create_task(self._run()) 113 | 114 | def __call__(self, interval): 115 | self.interval = interval 116 | 117 | async def _run(self): 118 | while True: 119 | await asyncio.sleep_ms(self.interval) 120 | # Do something useful here 121 | self.led.toggle() 122 | 123 | def go_fast(): # FooFunctor is available anywhere in this module 124 | FooFunctor(100) 125 | 126 | async def main(): 127 | FooFunctor(pyb.LED(1), 500) 128 | await asyncio.sleep(3) 129 | go_fast() 130 | await asyncio.sleep(3) 131 | 132 | asyncio.run(main()) 133 | ``` 134 | -------------------------------------------------------------------------------- /functor_singleton/examples.py: -------------------------------------------------------------------------------- 1 | def functor(cls): 2 | instance = None 3 | def getinstance(*args, **kwargs): 4 | nonlocal instance 5 | if instance is None: 6 | instance = cls(*args, **kwargs) 7 | return instance 8 | return instance(*args, **kwargs) 9 | return getinstance 10 | 11 | def singleton(cls): 12 | instance = None 13 | def getinstance(*args, **kwargs): 14 | nonlocal instance 15 | if instance is None: 16 | instance = cls(*args, **kwargs) 17 | return instance 18 | return getinstance 19 | 20 | @singleton 21 | class MySingleton: 22 | def __init__(self, arg): 23 | self.state = arg 24 | print('In __init__', arg) 25 | 26 | def foo(self, arg): 27 | print('In foo', arg + self.state) 28 | 29 | ms = MySingleton(42) # prints 'In __init__ 42' 30 | a = MySingleton(99) # No output: assign existing instance to a 31 | a.foo(5) # prints 'In foo 47': original state + 5 32 | 33 | 34 | @functor 35 | class MyFunctor: 36 | def __init__(self, arg): 37 | self.state = arg 38 | print('In __init__', arg) 39 | 40 | def __call__(self, arg): 41 | print('In __call__', arg + self.state) 42 | return self 43 | 44 | MyFunctor(42) # prints 'In __init__ 42' 45 | MyFunctor(5) # 'In __call__ 47' 46 | -------------------------------------------------------------------------------- /goertzel/README.md: -------------------------------------------------------------------------------- 1 | # Tone detection 2 | 3 | The [goertzel3.py](https://github.com/peterhinch/micropython-samples/blob/master/goertzel/goertzel3.py) 4 | implementation was written for the Pyboard but could be adapted for other 5 | platforms. It includes test tone generation. See code comments for more details. 6 | -------------------------------------------------------------------------------- /goertzel/goertzel3.py: -------------------------------------------------------------------------------- 1 | # Tone detection using Goertzel algorithm. 2 | # Requires Pyboard 1.x with X4 and X5 linked. 3 | 4 | from array import array 5 | import math 6 | import cmath 7 | from pyb import ADC, DAC, Pin, Timer 8 | import time 9 | import micropython 10 | import gc 11 | micropython.alloc_emergency_exception_buf(100) 12 | 13 | # When searching for a specific signal tone the Goertzel algorithm can be more efficient than fft. 14 | # This routine returns magnitude of a single fft bin, which contains the target frequency. 15 | 16 | # Constructor args: 17 | # freq (Hz) Target centre frequency 18 | # nsamples (int) Number of samples 19 | # spc (int) No. of samples per cycle of target frequency: sampling freq = freq * spc 20 | # Filter bandwidth as a proportion of target frequency is spc/nsamples 21 | # so 1KHz with 100 samples and 10 samples per cycle bw = 1KHz * 10/100 = 100Hz 22 | 23 | # .calc() computes magnitude of one DFT bin, then fires off a nonblocking acquisition of more data. 24 | # Depending on size of sample set, blocks for a few ms. 25 | 26 | class Goertzel: 27 | def __init__(self, adc, nsamples, freq, spc, verbose=False): 28 | if verbose: 29 | print('Freq {}Hz +- {}Hz'.format(freq, freq * spc / (2 * nsamples))) 30 | self.sampling_freq = freq * spc 31 | self.buf = array('H', (0 for _ in range(nsamples))) 32 | self.idx = 0 33 | self.nsamples = nsamples 34 | self.adc = adc 35 | self.scaling_factor = nsamples / 2.0 36 | omega = 2.0 * math.pi / spc 37 | self.coeff = 2.0 * math.cos(omega) 38 | self.fac = -math.cos(omega) + 1j * math.sin(omega) 39 | self.busy = False 40 | self.acquire() 41 | 42 | def acquire(self): 43 | self.busy = True 44 | self.idx = 0 45 | self.tim = Timer(6, freq=self.sampling_freq, callback=self.tcb) 46 | 47 | def tcb(self, _): 48 | buf = self.buf 49 | buf[self.idx] = self.adc.read() 50 | self.idx += 1 51 | if self.idx >= self.nsamples: 52 | self.tim.deinit() 53 | self.busy = False 54 | 55 | def calc(self): 56 | if self.busy: 57 | return False # Still acquiring data 58 | coeff = self.coeff 59 | buf = self.buf 60 | q0 = q1 = q2 = 0 61 | for s in buf: # Loop over 200 samples takes 3.2ms on Pyboard 1.x 62 | q0 = coeff * q1 - q2 + s 63 | q2 = q1 64 | q1 = q0 65 | self.acquire() # Start background acquisition 66 | return cmath.polar(q1 + q2 * self.fac)[0] / self.scaling_factor 67 | 68 | # Create a sinewave on pin X5 69 | def x5_test_signal(amplitude, freq): 70 | dac = DAC(1, bits=12) # X5 71 | buf = array('H', 2048 + int(amplitude * math.sin(2 * math.pi * i / 128)) for i in range(128)) 72 | tim = Timer(2, freq=freq * len(buf)) 73 | dac.write_timed(buf, tim, mode=DAC.CIRCULAR) 74 | return buf # Prevent deallocation 75 | 76 | def test(amplitude=2047): 77 | freq = 1000 78 | buf = x5_test_signal(amplitude, freq) 79 | adc = ADC(Pin.board.X4) 80 | g = Goertzel(adc, nsamples=100, freq=freq, spc=10, verbose=True) 81 | while True: 82 | time.sleep(0.5) 83 | print(g.calc()) 84 | -------------------------------------------------------------------------------- /import/IMPORT.md: -------------------------------------------------------------------------------- 1 | # MicroPython's import statement 2 | 3 | I seldom write tutorials on elementary Python coding; there are plenty online. 4 | However there are implications specific to low RAM environments. 5 | 6 | # 1. The import process 7 | 8 | When a module comprising Python source is imported, the compiler runs on the 9 | target and emits bytecode. The bytecode resides in RAM for later execution. 10 | Further, the compiler requires RAM, although this is reclaimed by the garbage 11 | collector after compilation is complete. The compilation stage may be skipped 12 | by precompiling the module to an `.mpy` file, but the only way to save on the 13 | RAM used by the bytecode is to use 14 | [frozen bytecode](http://docs.micropython.org/en/latest/reference/manifest.html). 15 | 16 | This doc addresses the case where code is not frozen, discussing ways to ensure 17 | that only necessary bytecode is loaded. 18 | 19 | # 2. Python packages and the lazy loader 20 | 21 | Python packages provide an excellent way to split a module into individual 22 | files to be loaded on demand. The drawback is that the user needs to know which 23 | file to import to access a particular item: 24 | ```python 25 | from my_library.foo import FooClass # It's in my_library/foo.py 26 | ``` 27 | This may be simplified using Damien's "lazy loader". This allows the user to 28 | write 29 | ```python 30 | import my_library 31 | foo = my_library.FooClass() # No need to know which file holds this class 32 | ``` 33 | The file `my_library/foo.py` is only loaded when it becomes clear that 34 | `FooClass` is required. Further, the structure of the package is hidden from 35 | the user and may be changed without affecting its API. 36 | 37 | The "lazy loader" is employed in 38 | [uasyncio](https://github.com/micropython/micropython/tree/master/extmod/uasyncio) 39 | making it possible to write 40 | ```python 41 | import uasyncio as asyncio 42 | e = asyncio.Event() # The file event.py is loaded now 43 | ``` 44 | Files are loaded as required. The source code is in `__init__.py`: 45 | [the lazy loader](https://github.com/micropython/micropython/blob/master/extmod/uasyncio/__init__.py). 46 | 47 | # 3. Wildcard imports 48 | 49 | The use of 50 | ```python 51 | from my_module import * 52 | ``` 53 | needs to be treated with caution for two reasons. It can populate the program's 54 | namespace with unexpected objects causing name conflicts. Secondly all these 55 | objects occupy RAM. In general wildcard imports should be avoided unless the 56 | module is designed to be imported in this way. For example issuing 57 | ```python 58 | from uasyncio import * 59 | ``` 60 | would defeat the lazy loader forcing all the files to be loaded. 61 | 62 | ## 3.1 Designing for wildcard import 63 | 64 | There are cases where wildcard import makes sense. For example a module might 65 | declare a number of constants. This module 66 | [colors.py](https://github.com/peterhinch/micropython-nano-gui/blob/master/gui/core/colors.py) 67 | computes a set of 13 colors for use in an interface. This is the module's only 68 | purpose so it is intended to be imported with 69 | ```python 70 | from gui.core.colors import * 71 | ``` 72 | This saves having to name each color explicitly. 73 | 74 | In larger modules it is wise to avoid populating the caller's namespace with 75 | cruft. This is achieved by ensuring that all private names begin with a `_` 76 | character. In a wildcard import, Python does not import such names. 77 | ```python 78 | _LOCAL_CONSTANT = const(42) 79 | def _foo(): 80 | print("foo") # A local function 81 | ``` 82 | Note that declaring a constant with a leading underscore saves RAM: no 83 | variable is created. The compiler substitues 42 when it sees `_LOCAL_CONSTANT`. 84 | Where there is no underscore the compiler still performs the same substitution, 85 | but a variable is created. This is because the module might be imported with a 86 | wildcard import. 87 | 88 | # 4. Reload 89 | 90 | Users coming from a PC background often query the lack of a `reload` command. 91 | In practice on a microcontroller it is usually best to issue a soft reset 92 | (`ctrl-d`) before re-importing an updated module. This is because a soft reset 93 | clears all retained state. However a `reload` function can be defined thus: 94 | ```python 95 | import gc 96 | import sys 97 | def reload(mod): 98 | mod_name = mod.__name__ 99 | del sys.modules[mod_name] 100 | gc.collect() 101 | __import__(mod_name) 102 | ``` 103 | ## 4.1 Indirect import 104 | 105 | The above code sample illustrates the method of importing a module where the 106 | module name is stored in a variable: 107 | ```python 108 | def run_test(module_name): 109 | mod = __import__(module_name) 110 | mod.test() # Run a self test 111 | ``` 112 | -------------------------------------------------------------------------------- /micropip/README.md: -------------------------------------------------------------------------------- 1 | # Overriding built in library modules 2 | 3 | Some firmware builds include library modules as frozen bytecode. On occasion it 4 | may be necessary to replace such a module with an updated or modified 5 | alternative. The most RAM-efficient solution is to rebuild the firmware with 6 | the replacement implemented as frozen bytecode. 7 | 8 | For users not wishing to recompile there is an alternative. The module search 9 | order is defined in `sys.path`. 10 | 11 | ``` 12 | >>> import sys 13 | >>> sys.path 14 | ['', '/flash', '/flash/lib'] 15 | ``` 16 | The `''` entry indicates that frozen modules will be found before those in the 17 | filesystem. This may be overridden by issuing: 18 | ``` 19 | >>> import sys 20 | >>> sys.path.append(sys.path.pop(0)) 21 | ``` 22 | This has the following outcome: 23 | ``` 24 | >>> sys.path 25 | ['/flash', '/flash/lib', ''] 26 | ``` 27 | Now modules in the filesystem will be compiled and executed in preference to 28 | those frozen as bytecode. 29 | -------------------------------------------------------------------------------- /mutex/README.md: -------------------------------------------------------------------------------- 1 | # A very simple mutex class. 2 | 3 | Files mutex.py mutex_test.py 4 | 5 | A mutex is an object enabling threads of execution to protect critical sections of code from reentrancy 6 | or to temporarily protect critical data sets from being updated by other threads. The term "mutex" 7 | derives from the notion of mutual exclusion. A mutex usually provides three methods: 8 | 9 | lock() (POSIX pthread_mutex_lock): wait until the mutex is free, then lock it 10 | unlock() (POSIX pthread_mutex_unlock): Immediate return. unlock the mutex. 11 | test() (POSIX pthread_mutex_trylock): Immediate return. test if it is locked. 12 | 13 | In this implementation lock and unlock is controlled by a context manager. 14 | 15 | In the context of MicroPython a mutex provides a mechanism where an interrupt service routine (ISR) can 16 | test whether the main loop was using critical variables at the time the interrupt occurred, and if so 17 | avoid modifying those variables. Typical use is as follows: 18 | 19 | ```python 20 | import pyb, mutex 21 | mutex = mutex.Mutex() 22 | data_ready = False 23 | 24 | def callback(): # Timer or interrupt callback 25 | global data_ready 26 | if mutex.test(): 27 | data_ready = True 28 | # Update critical variables 29 | mutex.release() 30 | else: 31 | # defer any update 32 | # Associate callback with device (pin or timer) 33 | 34 | while True: 35 | # code 36 | if data_ready: 37 | with mutex: 38 | data_ready = False 39 | # Access critical variables 40 | # more code 41 | ``` 42 | Note that the ``with`` statement will execute immediately because no other process runs a ``with`` block 43 | on the same mutex instance. 44 | 45 | [Linux man page](http://linux.die.net/man/3/pthread_mutex_lock) 46 | 47 | References describing mutex and semaphore objects 48 | [geeksforgeeks](http://www.geeksforgeeks.org/mutex-vs-semaphore/) 49 | [stackoverflow](http://stackoverflow.com/questions/62814/difference-between-binary-semaphore-and-mutex) 50 | [differencebetween](http://www.differencebetween.net/language/difference-between-mutex-and-semaphore/) 51 | -------------------------------------------------------------------------------- /mutex/mutex.py: -------------------------------------------------------------------------------- 1 | import pyb, micropython, array, uctypes 2 | micropython.alloc_emergency_exception_buf(100) 3 | 4 | class MutexException(OSError): 5 | pass 6 | 7 | class Mutex: 8 | @micropython.asm_thumb 9 | def _acquire(r0, r1): # Spinlock: wait on the semaphore. Return on success. 10 | label(LOOP) 11 | ldr(r0, [r1, 0]) # Wait for lock to be zero 12 | cmp(r0, 0) 13 | bne(LOOP) # Another process has the lock: spin on it 14 | cpsid(0) # OK, we have lock at this instant disable interrupts 15 | ldr(r0, [r1, 0]) # and re-check in case an interrupt occurred 16 | cmp(r0, 0) 17 | itt(ne) # if someone got in first re-enable ints 18 | cpsie(0) # and start polling again 19 | b(LOOP) 20 | mov(r0, 1) # We have an exclusive access 21 | str(r0, [r1, 0]) # set the lock 22 | cpsie(0) 23 | 24 | @micropython.asm_thumb 25 | def _attempt(r0, r1): # Nonblocking. Try to lock. Return 0 on success, 1 on fail 26 | cpsid(0) # disable interrupts 27 | ldr(r0, [r1, 0]) 28 | cmp(r0, 0) 29 | bne(FAIL) # Another process has the lock: fail 30 | mov(r2, 1) # No lock 31 | str(r2, [r1, 0]) # set the lock 32 | label(FAIL) 33 | cpsie(0) # enable interrupts 34 | 35 | def __init__(self): 36 | self.lock = array.array('i', (0,)) # 1 if a process has the lock else 0 37 | 38 | # POSIX API pthread_mutex_lock() blocks the thread till resource is available. 39 | def __enter__(self): 40 | self._acquire(uctypes.addressof(self.lock)) 41 | return self 42 | 43 | def __exit__(self, *_): 44 | self.lock[0] = 0 45 | 46 | # POSIX pthread_mutex_unlock() 47 | def release(self): 48 | if self.lock[0] == 0: 49 | raise MutexException('Semaphore already released') 50 | self.lock[0] = 0 51 | 52 | # POSIX pthread_mutex_trylock() API. When mutex is not available the function returns immediately 53 | def test(self): # Nonblocking: try to acquire, return True if success. 54 | return self._attempt(uctypes.addressof(self.lock)) == 0 55 | 56 | -------------------------------------------------------------------------------- /mutex/mutex_test.py: -------------------------------------------------------------------------------- 1 | import pyb, mutex 2 | 3 | mutex = mutex.Mutex() 4 | 5 | def update(t): # Interrupt handler may not be able to acquire the lock 6 | global var1, var2 # if main loop has it 7 | if mutex.test(): # critical section start 8 | var1 += 1 9 | pyb.udelay(200) 10 | var2 += 1 11 | mutex.release() # critical section end 12 | 13 | def main(): 14 | global var1, var2 15 | var1, var2 = 0, 0 16 | t2 = pyb.Timer(2, freq = 995, callback = update) 17 | t4 = pyb.Timer(4, freq = 1000, callback = update) 18 | for x in range(1000000): 19 | with mutex: # critical section start 20 | a = var1 21 | pyb.udelay(200) 22 | b = var2 23 | result = a == b # critical section end 24 | if not result: 25 | print('Fail after {} iterations'.format(x)) 26 | break 27 | pyb.delay(1) 28 | if x % 1000 == 0: 29 | print(x) 30 | t2.deinit() 31 | t4.deinit() 32 | print(var1, var2) 33 | -------------------------------------------------------------------------------- /ntptime/ntptime.py: -------------------------------------------------------------------------------- 1 | # Adapted from official ntptime by Peter Hinch July 2022 2 | # The main aim is portability: 3 | # Detects host device's epoch and returns time relative to that. 4 | # Basic approach to local time: add offset in hours relative to UTC. 5 | # Timeouts return a time of 0. These happen: caller should check for this. 6 | # Replace socket timeout with select.poll as per docs: 7 | # http://docs.micropython.org/en/latest/library/socket.html#socket.socket.settimeout 8 | 9 | import socket 10 | import struct 11 | import select 12 | from time import gmtime 13 | 14 | # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60 15 | # (date(1970, 1, 1) - date(1900, 1, 1)).days * 24*60*60 16 | NTP_DELTA = 3155673600 if gmtime(0)[0] == 2000 else 2208988800 17 | 18 | # The NTP host can be configured at runtime by doing: ntptime.host = 'myhost.org' 19 | host = "pool.ntp.org" 20 | 21 | def time(hrs_offset=0): # Local time offset in hrs relative to UTC 22 | NTP_QUERY = bytearray(48) 23 | NTP_QUERY[0] = 0x1B 24 | try: 25 | addr = socket.getaddrinfo(host, 123)[0][-1] 26 | except OSError: 27 | return 0 28 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 29 | poller = select.poll() 30 | poller.register(s, select.POLLIN) 31 | try: 32 | s.sendto(NTP_QUERY, addr) 33 | if poller.poll(1000): # time in milliseconds 34 | msg = s.recv(48) 35 | val = struct.unpack("!I", msg[40:44])[0] # Can return 0 36 | return max(val - NTP_DELTA + hrs_offset * 3600, 0) 37 | except OSError: 38 | pass # LAN error 39 | finally: 40 | s.close() 41 | return 0 # Timeout or LAN error occurred 42 | -------------------------------------------------------------------------------- /parse2d/README.md: -------------------------------------------------------------------------------- 1 | # Access a list or array as a 2D array 2 | 3 | The `parse2d` module provides a generator function `do_args`. This enables a 4 | generator to be instantiated which maps one or two dimensional address args 5 | onto index values. These can be used to address any object which can be 6 | represented as a one dimensional array including arrays, bytearrays, lists or 7 | random access files. The aim is to simplify writing classes that implement 2D 8 | arrays of objects - specifically writing `__getitem__` and `__setitem__` 9 | methods whose addressing modes conform with Python conventions. 10 | 11 | Addressing modes include slice objects; sets of elements can be accessed or 12 | populated without explicit iteration. Thus, if `demo` is an instance of a user 13 | defined class, the following access modes might be permitted: 14 | ```python 15 | demo = MyClass(10, 10) # Create an object that can be viewed as 10 rows * 10 cols 16 | demo[8, 8] = 42 # Populate a single cell 17 | demo[0:, 0] = iter((66, 77, 88)) # Populate a column 18 | demo[2:5, 2:5] = iter(range(50, 60)) # Populate a rectangular region. 19 | print(sum(demo[0, 0:])) # Sum a row 20 | for row, _ in enumerate(demo[0:, 0]): 21 | print(*demo[row, 0:]) # Size- and shape-agnostic print 22 | ``` 23 | The object can also be accessed as a 1D list. An application can mix and match 24 | 1D and 2D addressing as required: 25 | ```python 26 | demo[-1] = 999 # Set last element 27 | demo[-10: -1] = 7 # Set previous 9 elements 28 | ``` 29 | The focus of this module is convenience, minimising iteration and avoiding 30 | error-prone address calculations. It is not a high performance solution. The 31 | module resulted from guidance provided in 32 | [this discussion](https://github.com/orgs/micropython/discussions/11611). 33 | 34 | ###### [Main README](../README.md) 35 | 36 | # The do_args generator function 37 | 38 | This takes the following args: 39 | * `args` A 1- or 2-tuple. In the case of a 1-tuple (1D access) the element is 40 | an int or slice object. In the 2-tuple (2D) case each element can be an int or 41 | a slice. 42 | * `nrows` No. of rows in the array. 43 | * `ncols` No. of columns. 44 | 45 | This facilitates the design of `__getitem__` and `__setitem__`, e.g. 46 | ```python 47 | def __getitem__(self, *args): 48 | indices = do_args(args, self.nrows, self.ncols) 49 | for i in indices: 50 | yield self.cells[i] 51 | ``` 52 | The generator is agnostic of the meaning of the first and second args: the 53 | mathematical `[x, y]` or the graphics `[row, col]` conventions may be applied. 54 | Index values are `row * ncols + col` or `x * ncols + y` as shown by the 55 | following which must be run on CPython: 56 | ```python 57 | >>> g = do_args(((1, slice(0, 9)),), 10, 10) 58 | >>> for index in g: print(f"{index} ", end = "") 59 | 10 11 12 13 14 15 16 17 18 >>> 60 | ``` 61 | Three argument slices are supported: 62 | ```python 63 | >>> g = do_args(((1, slice(8, 0, -2)),), 10, 10) 64 | >>> for index in g: print(f"{index} ", end = "") 65 | ... 66 | 18 16 14 12 >>> 67 | ``` 68 | # Addressing 69 | 70 | The module aims to conform with Python rules. Thus, if `demo` is an instance of 71 | a class representing a 10x10 array, 72 | ```python 73 | print(demo[0, 10]) 74 | ``` 75 | will produce an `IndexError: Index out of range`. By contrast 76 | ```python 77 | print(demo[0, 0:100]) 78 | ``` 79 | will print row 0 (columns 0-9)without error. This is analogous to Python's 80 | behaviour with list addressing: 81 | ```python 82 | >>> l = [0] * 10 83 | >>> l[10] 84 | Traceback (most recent call last): 85 | File "", line 1, in 86 | IndexError: list index out of range 87 | >>> l[0:100] 88 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 89 | >>> 90 | ``` 91 | # Class design 92 | 93 | ## __getitem__() 94 | 95 | RAM usage can be minimised by having this return an iterator. This enables 96 | usage such as `sum(arr[0:, 1])` and minimises RAM allocation. 97 | 98 | ## __setitem__() 99 | 100 | The semantics of the right hand side of assignment expressions is defined in 101 | the user class. The following uses a `list` as the addressable object. 102 | ```python 103 | from parse2d import do_args 104 | 105 | class MyIntegerArray: 106 | def __init__(self, nrows, ncols): 107 | self.nrows = nrows 108 | self.ncols = ncols 109 | self.cells = [0] * nrows * ncols 110 | 111 | def __getitem__(self, *args): 112 | indices = do_args(args, self.nrows, self.ncols) 113 | for i in indices: 114 | yield self.cells[i] 115 | 116 | def __setitem__(self, *args): 117 | value = args[1] 118 | indices = do_args(args[: -1], self.nrows, self.ncols) 119 | for i in indices: 120 | self.cells[i] = value 121 | ``` 122 | The `__setitem__` method is minimal. In a practical class `value` might be a 123 | `list`, `tuple` or an object supporting the iterator protocol. 124 | 125 | # The int2D demo 126 | 127 | RHS semantics differ from Python `list` practice in that you can populate an 128 | entire row with 129 | ```python 130 | demo[0:, 0] = iter((66, 77, 88)) 131 | ``` 132 | If the iterator runs out of data, the last item is repeated. Equally you could 133 | have 134 | ```python 135 | demo[0:, 0] = 4 136 | ``` 137 | The demo also illustrates the case where `__getitem__` returns an iterator. 138 | 139 | ###### [Main README](../README.md) 140 | -------------------------------------------------------------------------------- /parse2d/demo_parse2d.py: -------------------------------------------------------------------------------- 1 | # demo_parse2d.py Parse args for item access dunder methods for a 2D array. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023 Peter Hinch 5 | 6 | from parse2d import do_args 7 | 8 | class int2D: 9 | def __init__(self, nrows, ncols): 10 | self.nrows = nrows 11 | self.ncols = ncols 12 | self.cells = [0] * nrows * ncols 13 | 14 | def __getitem__(self, *args): 15 | indices = do_args(args, self.nrows, self.ncols) 16 | for i in indices: 17 | yield self.cells[i] 18 | 19 | def __setitem__(self, *args): 20 | x = args[1] # Value 21 | indices = do_args(args[: -1], self.nrows, self.ncols) 22 | for i in indices: 23 | if isinstance(x, int): 24 | self.cells[i] = x 25 | else: 26 | try: 27 | z = next(x) # Will throw if not an iterator or generator 28 | except StopIteration: 29 | pass # Repeat last value 30 | self.cells[i] = z 31 | 32 | demo = int2D(10, 10) 33 | demo[0, 1:5] = iter(range(10, 20)) 34 | print(next(demo[0, 0:])) 35 | demo[0:, 0] = iter((66,77,88)) 36 | print(next(demo[0:, 0])) 37 | demo[8, 8] = 42 38 | #demo[8, 10] = 9999 # Index out of range 39 | demo[2:5, 2:5] = iter(range(50, 60)) 40 | for row in range(10): 41 | print(*demo[row, 0:]) 42 | 43 | 44 | # 1D addressing 45 | # g = do_args((30,), 10, 10) # 30 46 | # g = do_args((slice(30, 34),), 10, 10) # 30 31 32 33 47 | # 2D addressing 48 | # g = do_args(((1, slice(0, 9)),), 10, 10) # 10 11 12 ... 18 49 | # g = do_args(((1, 2),), 10, 10) # 10 50 | -------------------------------------------------------------------------------- /parse2d/parse2d.py: -------------------------------------------------------------------------------- 1 | # parse2d.py Parse args for item access dunder methods for a 2D array. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023 Peter Hinch 5 | 6 | 7 | # Called from __getitem__ or __setitem__ args is a 1-tuple. The single item may be an int or a 8 | # slice for 1D access. Or it may be a 2-tuple for 2D access. Items in the 2-tuple may be ints 9 | # or slices in any combination. 10 | # As a generator it returns offsets into the underlying 1D array or list. 11 | def do_args(args, nrows, ncols): 12 | # Given a slice and a maximum address return start and stop addresses (or None on error) 13 | # Step value must be 1, hence does not support start > stop (used with step < 0) 14 | def do_slice(sli, nbytes): 15 | step = sli.step if sli.step is not None else 1 16 | start = sli.start if sli.start is not None else 0 17 | stop = sli.stop if sli.stop is not None else nbytes 18 | start = min(start if start >= 0 else max(nbytes + start, 0), nbytes) 19 | stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes) 20 | ok = (start < stop and step > 0) or (start > stop and step < 0) 21 | return (start, stop, step) if ok else None # Caller should check 22 | 23 | def ivalid(n, nmax): # Validate an integer arg, handle -ve args 24 | n = n if n >= 0 else nmax + n 25 | if n < 0 or n > nmax - 1: 26 | raise IndexError("Index out of range") 27 | return n 28 | 29 | def fail(n): 30 | raise IndexError("Invalid index", n) 31 | 32 | ncells = nrows * ncols 33 | n = args[0] 34 | if isinstance(n, int): # Index into 1D array 35 | yield ivalid(n, ncells) 36 | elif isinstance(n, slice): # Slice of 1D array 37 | cells = do_slice(n, ncells) 38 | if cells is not None: 39 | for cell in range(*cells): 40 | yield cell 41 | elif isinstance(n, tuple): # or isinstance(n, list) old versions of grid 42 | if len(n) != 2: 43 | fail(n) 44 | row = n[0] # May be slice 45 | if isinstance(row, int): 46 | row = ivalid(row, nrows) 47 | col = n[1] 48 | if isinstance(col, int): 49 | col = ivalid(col, ncols) 50 | if isinstance(row, int) and isinstance(col, int): 51 | yield row * ncols + col 52 | elif isinstance(row, slice) and isinstance(col, int): 53 | rows = do_slice(row, nrows) 54 | if rows is not None: 55 | for row in range(*rows): 56 | yield row * ncols + col 57 | elif isinstance(row, int) and isinstance(col, slice): 58 | cols = do_slice(col, ncols) 59 | if cols is not None: 60 | for col in range(*cols): 61 | yield row * ncols + col 62 | elif isinstance(row, slice) and isinstance(col, slice): 63 | rows = do_slice(row, nrows) 64 | cols = do_slice(col, ncols) 65 | if cols is not None and rows is not None: 66 | for row in range(*rows): 67 | for col in range(*cols): 68 | yield row * ncols + col 69 | else: 70 | fail(n) 71 | else: 72 | fail(n) 73 | -------------------------------------------------------------------------------- /phase/README.md: -------------------------------------------------------------------------------- 1 | # 1. Introduction 2 | 3 | The principal purpose of this application note is to describe a technique for 4 | measuring the relative phase of a pair of sinsusoidal signals over the full 5 | range of 2π radians (360°). This is known as quadrature detection; while 6 | ancient history to radio engineers the method may be unfamiliar to those from 7 | a programming background. 8 | 9 | ## 1.1 Measurement of relative timing and phase of analog signals 10 | 11 | As of 11th April 2018 the Pyboard firmware has been enhanced to enable multiple 12 | ADC channels to be read in response to a timer tick. At each tick a reading is 13 | taken from each ADC in quick succession. This enables the relative timing or 14 | phase of signals to be measured. This is facilitated by the static method 15 | `ADC.read_timed_multi` which is documented 16 | [here](http://docs.micropython.org/en/latest/pyboard/library/pyb.ADC.html). 17 | 18 | The ability to perform such measurements substantially increases the potential 19 | application areas of the Pyboard, supporting precision measurements of signals 20 | into the ultrasonic range. Applications such as ultrasonic rangefinders may be 21 | practicable. With two or more microphones it may be feasible to produce an 22 | ultrasonic active sonar capable of providing directional and distance 23 | information for multiple targets. 24 | 25 | I have used it to build an electrical network analyser which yields accurate 26 | gain and phase (+-3°) plots of signals up to 40KHz. 27 | 28 | # 2 Applications 29 | 30 | ## 2.1 Measurements of relative timing 31 | 32 | In practice `ADC.read_timed_multi` reads each ADC in turn. This implies a delay 33 | between each reading. This was estimated at 1.8μs on a Pyboard V1.1. This value 34 | can be used to compensate any readings taken. 35 | 36 | ## 2.2 Phase measurements 37 | 38 | ### 2.2.1 The quadrature detector 39 | 40 | The principle of a phase sensitive detector (applicable to linear and sampled 41 | data systems) is based on multiplying the two signals and low pass filtering 42 | the result. This derives from the prosthaphaeresis formula: 43 | 44 | sin a sin b = (cos(a-b) - cos(a+b))/2 45 | 46 | If 47 | ω = angular frequency in rad/sec 48 | t = time 49 | ϕ = phase 50 | this can be written: 51 | 52 | sin ωt sin(ωt + ϕ) = 0.5(cos(-ϕ) - cos(2ωt + ϕ)) 53 | 54 | The first term on the right hand side is a DC component related to the relative 55 | phase of the two signals. The second is an AC component at twice the incoming 56 | frequency. So if the product signal is passed through a low pass filter the 57 | right hand term disappears leaving only 0.5cos(-ϕ). 58 | 59 | Where the frequency is known the filtering may be achieved simply by averaging 60 | over an integer number of cycles. 61 | 62 | For the above to produce accurate phase measurements the amplitudes of the two 63 | signals must be normalised to 1. Alternatively the amplitudes may be measured 64 | and the DC phase value divided by their product. 65 | 66 | Because cos ϕ = cos -ϕ this can only detect phase angles over a range of π 67 | radians. To achieve detection over the full 2π range a second product detector 68 | is used with one signal phase-shifted by π/2. This allows a complex phasor 69 | (phase vector) to be derived, with one detector providing the real part and the 70 | other the imaginary one. 71 | 72 | In a sampled data system where the frequency is known, the phase shifted signal 73 | may be derived by indexing into one of the sample arrays. To achieve this the 74 | signals must be sampled at a rate of 4Nf where f is the frequency and N is an 75 | integer >= 1. In the limiting case where N == 1 the index offset is 1; this 76 | sampling rate is double the Nyquist rate. 77 | 78 | In practice phase compensation may be required to allow for a time delay 79 | between sampling the two signals. If the delay is T and the frequency is f, the 80 | phase shift θ is given by 81 | 82 | θ = 2πfT 83 | 84 | Conventionally phasors rotate anticlockwise for leading phase. A time delay 85 | implies a phase lag i.e. a negative phase or a clockwise rotation. If λ is the 86 | phasor derived above, the adjusted phase α is given by multiplying by a phasor 87 | of unit magnitude and phase -θ: 88 | 89 | α = λ(cos θ - jsin θ) 90 | 91 | For small angles (i.e. at lower frequencies) this approximates to 92 | 93 | α ~= λ(1 - jθ) 94 | 95 | ### 2.2.2 A MicroPython implementation 96 | 97 | The example below, taken from an application, uses quadrature detection to 98 | accurately measure the phase difference between an outgoing sinewave produced 99 | by `DAC.write_timed` and an incoming response signal. For application reasons 100 | `DAC.write_timed` runs continuously. Its output feeds one ADC and the incoming 101 | signal feeds another. The ADC's are fed from matched hardware anti-aliasing 102 | filters; the matched characteristic ensures that any phase shift in the filters 103 | cancels out. 104 | 105 | Because the frequency is known the ADC sampling rate is chosen so that an 106 | integer number of cycles are captured. Thus averaging is used to remove the 107 | double frequency component. 108 | 109 | The function `demod()` returns the phase difference in radians. The sample 110 | arrays are globals `bufout` and `bufin`. The `freq` arg is the ADC sampling 111 | frequency and is used to provide phase compensation for the delay mentioned in 112 | section 2.1. 113 | 114 | The arg `nsamples` is the number of samples per cycle of the sinewave. As 115 | described above it can be any integer multiple of 4. 116 | 117 | ```python 118 | from math import sqrt, pi, sin, cos 119 | import cmath 120 | _ROOT2 = sqrt(2) 121 | 122 | # Return RMS value of a buffer, removing DC. 123 | def amplitude(buf): 124 | buflen = len(buf) 125 | meanin = sum(buf)/buflen 126 | return sqrt(sum((x - meanin)**2 for x in buf)/buflen) 127 | 128 | def demod(freq, nsamples): 129 | sum_norm = 0 130 | sum_quad = 0 # quadrature pi/2 phase shift 131 | buflen = len(bufin) 132 | assert len(bufout) == buflen, 'buffer lengths must match' 133 | meanout = sum(bufout)/buflen # ADC samples are DC-shifted 134 | meanin = sum(bufin)/buflen 135 | # Remove DC offset, calculate RMS and convert to peak value (sine assumption) 136 | # Aim: produce sum_norm and sum_quad in range -1 <= v <= +1 137 | peakout = amplitude(bufout) * _ROOT2 138 | peakin = amplitude(bufin) * _ROOT2 139 | # Calculate phase 140 | delta = int(nsamples // 4) # offset for pi/2 141 | for x in range(buflen): 142 | v0 = (bufout[x] - meanout) / peakout 143 | v1 = (bufin[x] - meanin) / peakin 144 | s = (x + delta) % buflen # + pi/2 145 | v2 = (bufout[s] - meanout) / peakout 146 | sum_norm += v0 * v1 # Normal 147 | sum_quad += v2 * v1 # Quadrature 148 | 149 | sum_norm /= (buflen * 0.5) # Factor of 0.5 from the trig formula 150 | sum_quad /= (buflen * 0.5) 151 | c = sum_norm + 1j * sum_quad # Create the complex phasor 152 | # Apply phase compensation measured at 1.8μs 153 | theta = 2 * pi * freq * 1.8e-6 154 | c *= cos(theta) - 1j * sin(theta) 155 | return cmath.phase(c) 156 | ``` 157 | Note that the phase compensation figure of 1.8μs is based on empirical 158 | measurement on a Pyboard 1.x. The easiest way to measure the time difference 159 | between samples is to measure the phase error between identical fast sinewaves. 160 | This was done by feeding the same signal into both ADC's (with compensation 161 | disabled) and measuring the reported phase difference. Then 162 | T = θ/(2πf) where f is the signal frequency. 163 | -------------------------------------------------------------------------------- /power/README.md: -------------------------------------------------------------------------------- 1 | # A phasor power meter 2 | 3 | This measures the AC mains power consumed by a device. Unlike many cheap power 4 | meters it performs a vector measurement and can display true power, VA and 5 | phase. It can also plot snapshots of voltage and current waveforms. It can 6 | calculate average power consumption of devices whose consumption varies with 7 | time such as freezers and washing machines, and will work with devices capable 8 | of sourcing power into the grid. It supports full scale ranges of 30W to 3KW. 9 | 10 | [Images of device](./images/IMAGES.md) 11 | 12 | ###### [Main README](../README.md) 13 | 14 | ## Warning 15 | 16 | This project includes mains voltage wiring. Please don't attempt it unless you 17 | have the necessary skills and experience to do this safely. 18 | 19 | # Hardware Overview 20 | 21 | The file `SignalConditioner.fzz` includes the schematic and PCB layout for the 22 | device's input circuit. The Fritzing utility required to view and edit this is 23 | available (free) from [here](http://fritzing.org/download/). 24 | 25 | The unit includes a transformer with two 6VAC secondaries. One is used to power 26 | the device, the other to measure the line voltage. Current is measured by means 27 | of a current transformer SEN-11005 from SparkFun. The current output from this 28 | is converted to a voltage by means of an op-amp configured as a transconductance 29 | amplifier. This passes through a variable gain amplifier comprising two cascaded 30 | MCP6S91 programmable gain amplifiers, then to a two pole Butterworth low pass 31 | anti-aliasing filter. The resultant signal is presented to one of the Pyboard's 32 | shielded ADC's. The transconductance amplifier also acts as a single pole low 33 | pass filter. 34 | 35 | The voltage signal is similarly filtered with three matched poles to ensure 36 | that correct relative phase is maintained. The voltage channel has fixed gain. 37 | 38 | ## PCB 39 | 40 | The PCB and schematic have an error in that the inputs of the half of opamp U4 41 | which handles the current signal are transposed. 42 | 43 | # Firmware Overview 44 | 45 | ## Dependencies 46 | 47 | 1. The `uasyncio` library. 48 | 2. The official lcd160 driver `lcd160cr.py`. 49 | 50 | Also from the [lcd160cr GUI library](https://github.com/peterhinch/micropython-lcd160cr-gui.git) 51 | the following files: 52 | 53 | 1. `lcd160_gui.py`. 54 | 2. `font10.py`. 55 | 3. `lcd_local.py` 56 | 4. `constants.py` 57 | 5. `lplot.py` 58 | 59 | ## Configuration 60 | 61 | In my build the above plus `mains.py` are implemented as frozen bytecode. There 62 | is no SD card, the flash filesystem containing `main.py` and `mt.py`. 63 | 64 | If `mt.py` is deleted from flash and located on an SD card the code will create 65 | simulated sinewave samples for testing. 66 | 67 | ## Design 68 | 69 | The code has not been optimised for performance, which in my view is adequate 70 | for the application. 71 | 72 | The module `mains.py` contains two classes, `Preprocessor` and `Scaling` which 73 | perform the data capture and analysis. The former acquires the data, normalises 74 | it and calculates normalised values of RMS voltage and current along with power 75 | and phase. `Scaling` controls the PGA according to the selected range and 76 | modifies the Vrms, Irms and P values to be in conventional units. 77 | 78 | The `Scaling` instance created in `mt.py` has a continuously running coroutine 79 | (`._run()`) which reads a set of samples, processes them, and executes a 80 | callback. Note that the callback function is changed at runtime by the GUI code 81 | (by `mains_device.set_callback()`). The iteration rate of `._run()` is about 82 | 1Hz. 83 | 84 | The code is intended to offer a degree of noise immunity, in particular in the 85 | detection of voltage zero crossings. It operates by acquiring a set of 400 86 | sample pairs (voltage and current) as fast as standard MicroPython can achieve. 87 | On the Pyboard with 50Hz mains this captures two full cycles, so guaranteeing 88 | two positive going voltage zero crossings. The code uses an averaging algorithm 89 | to detect these (`Preprocessor.analyse()`) and populates four arrays of floats 90 | with precisely one complete cycle of data. The arrays comprise two pairs of 91 | current and voltage values, one scaled for plotting and the other scaled for 92 | measurement. 93 | 94 | Both pairs are scaled to a range of +-1.0 with any DC bias removed (owing to 95 | the presence of transformers this can only arise due to offsets in the 96 | circuitry and/or ADC's). DC removal facilitates long term integration. 97 | 98 | Plot data is further normalised so that current values exactly fill the +-1.0 99 | range. In other words plots are scaled so that the current waveform fills the 100 | Y axis with the X axis containing one full cycle. The voltage plot is made 10% 101 | smaller to avoid the visually confusing situation with a resistive load where 102 | the two plots coincide exactly. 103 | 104 | ## Calibration 105 | 106 | This is defined by `Scaling.vscale` and `Scaling.iscale`. These values were 107 | initially calculated, then adjusted by comparing voltage and current readings 108 | with measurements from a calibrated meter. Voltage calibration in particular 109 | will probably need adjusting depending on the transformer characteristics. 110 | -------------------------------------------------------------------------------- /power/SignalConditioner.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/SignalConditioner.fzz -------------------------------------------------------------------------------- /power/images/IMAGES.md: -------------------------------------------------------------------------------- 1 | # Power Meter Sample Images 2 | 3 | ![Exterior](./outside.JPG) 4 | 5 | ![Interior](./interior.JPG) 6 | Interior construction - slightly out of date: doesn't show cable glands. 7 | 8 | ## Screenshots. 9 | 10 | **These look much better in reality than in my pictures.** 11 | 12 | ![Microwave](./microwave.JPG) 13 | Microwave oven. 14 | ![Plot](./plot.JPG) 15 | Microwave waveforms. 16 | 17 | ![Integrate](./integrate.JPG) 18 | Integration screen. 19 | 20 | ![Underrange](./underrange.JPG) 21 | Soldering iron on 3KW range. 22 | 23 | ![Correct range](./correctrange.JPG) 24 | Same iron on 30W range. 25 | 26 | -------------------------------------------------------------------------------- /power/images/correctrange.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/correctrange.JPG -------------------------------------------------------------------------------- /power/images/integrate.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/integrate.JPG -------------------------------------------------------------------------------- /power/images/interior.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/interior.JPG -------------------------------------------------------------------------------- /power/images/microwave.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/microwave.JPG -------------------------------------------------------------------------------- /power/images/outside.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/outside.JPG -------------------------------------------------------------------------------- /power/images/plot.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/plot.JPG -------------------------------------------------------------------------------- /power/images/underrange.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/power/images/underrange.JPG -------------------------------------------------------------------------------- /pyboard_d/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial guide to the Pyboard D 2 | 3 | Note: official docs may now be found [here](https://pybd.io/hw/pybd_sfxw.html) 4 | so I expect to remove this guide soon. 5 | 6 | ## LED's 7 | 8 | The board has one RGB led. Each colour is addressed as pyb.LED(n) where n is in 9 | range 1 (R) to 3 (B). 10 | 11 | ## Accel 12 | 13 | These boards do not have an accelerometer. 14 | 15 | ## WiFi 16 | 17 | After a power cycle a connection must be established with explicit credentials: 18 | the board behaves more like ESP32 than ESP8266. If a WiFi outage occurs it will 19 | attempt automatically to reconnect. The following code fragments may be used. 20 | 21 | ```python 22 | wl = network.WLAN() 23 | wl.connect(my_ssid, my_password) 24 | wl.active(1) 25 | print(wl) 26 | ``` 27 | It can be in state `down`, `join` or `up`. `down` means that it's not trying to 28 | connect. `join` means it's trying to connect and get an IP address, and `up` 29 | means it's connected with an IP address and ready to send/receive. If the AP 30 | disappears then it goes from `up` to `join`, and will go back to `up` if the AP 31 | reappears. `wl.status()` will give numeric values of these states: 32 | 0=`down`, 1 and 2 mean `join` (different variants of it), 3=``up`. 33 | 34 | You can also debug the wlan using tracing: 35 | ```python 36 | wl = network.WLAN() 37 | wl.config(trace=value) 38 | ``` 39 | `value` can be a bit-wise or of 1=async-events, 2=eth-tx, 4=eth-rx. So: 40 | ```python 41 | wl = network.WLAN() 42 | wl.config(trace=7) # To see everything. 43 | wl.config(trace=0) # To stop 44 | ``` 45 | This will work on both STA and AP interfaces, so you can watch how two PYBD's 46 | connect to each other. 47 | 48 | Setting antenna type and TX power 49 | ```python 50 | wl = network.WLAN() 51 | wl.config(antenna=value) # 0 internal 1 external 52 | wl.config(txpower=value) # In dbm 53 | ``` 54 | 55 | ## Flash memory 56 | 57 | The SF2W and SF3W have 512KiB of internal flash, the SF6W has 2048KiB. All 58 | have two external 2048KiB flash chips, one for the filesystem and the other for 59 | executable code (it can be memory mapped). On SF2W and SF3W, if you freeze a 60 | lot of code the firmware can become too big for the internal flash. To put 61 | frozen code in external flash, edit the file 62 | `ports/stm32/boards/PYBD_SF2/f722_qspi.ld` (or the corresponding one for 63 | `PYBD_SF3` to add the line `*frozen_content.o(.text* .rodata*)`: 64 | ``` 65 | .text_ext : 66 | { 67 | . = ALIGN(4); 68 | *frozen_content.o(.text* .rodata*) 69 | *lib/btstack/*(.text* .rodata*) 70 | *lib/mbedtls/*(.text* .rodata*) 71 | . = ALIGN(512); 72 | *(.big_const*) 73 | 74 | ``` 75 | 76 | There is a [small performance penalty](https://forum.micropython.org/viewtopic.php?f=16&t=8767#p49507) 77 | in doing this of around 10%. 78 | 79 | ## Bootloader and boot options 80 | 81 | You can boot into special modes by holding down the usr button and briefly 82 | pressing reset. The LED flashes in sequence: red, green, blue, white. The boot 83 | mode is determined by the color showing at the time when you release the usr 84 | button. 85 | 86 | 1. red: Normal 87 | 2. green: safe boot (don't execute boot.py or main.py) 88 | 3. blue: factory reset. Re-initialises the filesystem on /flash wiping any 89 | user files. Does not affect the SD card. 90 | 4. white: bootloader mode for firmware update. The red LED then flashes once a 91 | second indicating bootloader mode. 92 | 93 | You can also put the board in booloader mode by executing pyb.bootloader(). 94 | 95 | Once in booloader mode upload the firmware as usual: 96 | ```bash 97 | tools/pydfu.py -u `path_to_firmware` 98 | ``` 99 | 100 | ## Code emitters 101 | 102 | Native, Viper and inline Arm Thumb assembler features are supported. 103 | 104 | ## SD Card 105 | 106 | Unlike the Pyboard 1.x this is not mounted by default. To auto-mount it, 107 | include the following in `boot.py`: 108 | 109 | ```python 110 | import sys, os, pyb 111 | 112 | if pyb.SDCard().present(): 113 | os.mount(pyb.SDCard(), '/sd') 114 | sys.path[1:1] = ['/sd', '/sd/lib'] 115 | ``` 116 | -------------------------------------------------------------------------------- /quaternion/graph3d.py: -------------------------------------------------------------------------------- 1 | # graph3d.py 3D graphics demo of quaternion use 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | import gc 7 | from math import pi 8 | from quat import Rotator, Point 9 | from setup3d import fill, line, show, DIMENSION 10 | 11 | class Line: 12 | def __init__(self, p0, p1, color): 13 | #assert p0.isvec() and p1.isvec() 14 | self.start = p0 15 | self.end = p1 16 | self.color = color 17 | 18 | def show(self, ssd): 19 | _, xs, ys, zs = self.start 20 | _, xe, ye, ze = self.end 21 | # Handle perspective and scale to display 22 | w = DIMENSION # Viewing area is square 23 | h = w 24 | xs = round((1 + xs/zs) * w) 25 | ys = round((1 - ys/zs) * h) 26 | xe = round((1 + xe/ze) * w) 27 | ye = round((1 - ye/ze) * h) 28 | line(xs,ys, xe, ye, self.color) 29 | 30 | def __add__(self, to): # to is a Point or 3-tuple 31 | return Line(self.start + to, self.end + to, self.color) 32 | 33 | def __sub__(self, v): # to is a Point or 3-tuple 34 | return Line(self.start - v, self.end - v, self.color) 35 | 36 | def __mul__(self, by): # by is a 3-tuple 37 | return Line(self.start * by, self.end * by, self.color) 38 | 39 | def __matmul__(self, rot): # rot is a rotation quaternion 40 | #assert rot.isrot() 41 | return Line(self.start @ rot, self.end @ rot, self.color) 42 | 43 | def camera(self, rot, distance): # rot is a rotation quaternion, distance is scalar 44 | #assert rot.isrot() 45 | gc.collect() 46 | ps = self.start @ rot 47 | ps = Point(ps.x * distance, ps.y * distance, distance - ps.z) 48 | pe = self.end @ rot 49 | pe = Point(pe.x * distance, pe.y * distance, distance - pe.z) 50 | return Line(ps, pe, self.color) 51 | 52 | def __str__(self): 53 | return 'start {} end {}'.format(self.start, self.end) 54 | 55 | class Shape: 56 | def __init__(self, lines): 57 | self.lines = lines 58 | 59 | def __add__(self, to): 60 | return Shape([l + to for l in self.lines]) 61 | 62 | def __sub__(self, v): 63 | return Shape([l - v for l in self.lines]) 64 | 65 | def __mul__(self, by): 66 | return Shape([l * by for l in self.lines]) 67 | 68 | def __matmul__(self, rot): 69 | l = [] 70 | for line in self.lines: 71 | l.append(line @ rot) 72 | return Shape(l) 73 | 74 | def camera(self, rot, distance): 75 | l = [] 76 | for line in self.lines: 77 | l.append(line.camera(rot, distance)) 78 | return Shape(l) 79 | 80 | def show(self, ssd): 81 | for line in self.lines: 82 | line.show(ssd) 83 | 84 | def __str__(self): 85 | r = '' 86 | for line in self.lines: 87 | r = ''.join((r, '{}\n'.format(line))) 88 | return r 89 | 90 | class Axes(Shape): 91 | def __init__(self, color): 92 | l = (Line(Point(-1.0, 0, 0), Point(1.0, 0, 0), color), 93 | Line(Point(0, -1.0, 0), Point(0, 1.0, 0), color), 94 | Line(Point(0, 0, -1.0), Point(0, 0, 1.0), color)) 95 | super().__init__(l) 96 | 97 | class Square(Shape): # Unit square in XY plane 98 | def __init__(self, color): # Corner located at origin 99 | l = (Line(Point(0, 0, 0), Point(1, 0, 0), color), 100 | Line(Point(1, 0, 0), Point(1, 1, 0), color), 101 | Line(Point(1, 1, 0), Point(0, 1, 0), color), 102 | Line(Point(0, 1, 0), Point(0, 0, 0), color)) 103 | super().__init__(l) 104 | 105 | class Cube(Shape): 106 | def __init__(self, color, front=None, sides=None): # Corner located at origin 107 | front = color if front is None else front 108 | sides = color if sides is None else sides 109 | l = (Line(Point(0, 0, 0), Point(1, 0, 0), color), 110 | Line(Point(1, 0, 0), Point(1, 1, 0), color), 111 | Line(Point(1, 1, 0), Point(0, 1, 0), color), 112 | Line(Point(0, 1, 0), Point(0, 0, 0), color), 113 | Line(Point(0, 0, 1), Point(1, 0, 1), front), 114 | Line(Point(1, 0, 1), Point(1, 1, 1), front), 115 | Line(Point(1, 1, 1), Point(0, 1, 1), front), 116 | Line(Point(0, 1, 1), Point(0, 0, 1), front), 117 | Line(Point(0, 0, 0), Point(0, 0, 1), sides), 118 | Line(Point(1, 0, 0), Point(1, 0, 1), sides), 119 | Line(Point(1, 1, 0), Point(1, 1, 1), sides), 120 | Line(Point(0, 1, 0), Point(0, 1, 1), sides), 121 | ) 122 | super().__init__(l) 123 | 124 | class Cone(Shape): 125 | def __init__(self, color, segments=12): 126 | rot = Rotator(2*pi/segments, 0, 1, 0) 127 | p0 = Point(1, 1, 0) 128 | p1 = p0.copy() 129 | orig = Point(0, 0, 0) 130 | lines = [] 131 | for _ in range(segments + 1): 132 | p1 @= rot 133 | lines.append(Line(p0, p1, color)) 134 | lines.append(Line(orig, p0, color)) 135 | p0 @= rot 136 | super().__init__(lines) 137 | 138 | class Circle(Shape): # Unit circle in XY plane centred on origin 139 | def __init__(self, color, segments=12): 140 | rot = Rotator(2*pi/segments, 0, 1, 0) 141 | p0 = Point(1, 0, 0) 142 | p1 = p0.copy() 143 | lines = [] 144 | for _ in range(segments + 1): 145 | p1 @= rot 146 | lines.append(Line(p0, p1, color)) 147 | p0 @= rot 148 | super().__init__(lines) 149 | 150 | class Sphere(Shape): # Unit sphere in XY plane centred on origin 151 | def __init__(self, color, segments=12): 152 | lines = [] 153 | s = Circle(color) 154 | xrot = Rotator(2 * pi / segments, 1, 0, 0) 155 | for _ in range(segments / 2 + 1): 156 | gc.collect() 157 | lines.extend(s.lines[:]) 158 | s @= xrot 159 | super().__init__(lines) 160 | 161 | 162 | # Composition rather than inheritance as MP can't inherit builtin types. 163 | class DisplayDict: 164 | def __init__(self, ssd, angle, distance): 165 | self.ssd = ssd 166 | self.distance = distance # scalar 167 | # Rotation quaternion for camera view 168 | self.crot = Rotator(angle, 1, 1, 0) 169 | self.d = {} 170 | 171 | def __setitem__(self, key, value): 172 | if not isinstance(value, Shape): 173 | raise ValueError('DisplayDict entries must be Shapes') 174 | self.d[key] = value 175 | 176 | def __getitem__(self, key): 177 | return self.d[key] 178 | 179 | def __delitem__(self, key): 180 | del self.d[key] 181 | 182 | def show(self): 183 | ssd = self.ssd 184 | fill(0) 185 | crot = self.crot 186 | dz = self.distance 187 | for shape in self.d.values(): 188 | s = shape.camera(crot, dz) 189 | s.show(ssd) 190 | show() 191 | -------------------------------------------------------------------------------- /quaternion/quat.py: -------------------------------------------------------------------------------- 1 | # quat.py "Micro" Quaternion class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | from math import sqrt, sin, cos, acos, isclose, asin, atan2, pi 7 | from array import array 8 | mdelta = 0.001 # 0.1% Minimum difference considered significant for graphics 9 | adelta = 0.001 # Absolute tolerance for components near 0 10 | 11 | def _arglen(arg): 12 | length = 0 13 | try: 14 | length = len(arg) 15 | except TypeError: 16 | pass 17 | if length not in (0, 3, 4): 18 | raise ValueError('Sequence length must be 3 or 4') 19 | return length 20 | 21 | # Convert a rotation quaternion to Euler angles. Beware: 22 | # https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible 23 | def euler(q): # Return (heading, pitch, roll) 24 | if not q.isrot(): 25 | raise ValueError('Must be a rotation quaternion.') 26 | w, x, y, z = q 27 | pitch = asin(2*(w*y - x*z)) 28 | if isclose(pitch, pi/2, rel_tol = mdelta): 29 | return -2 * atan2(x, w), pitch, 0 30 | if isclose(pitch, -pi/2, rel_tol = mdelta): 31 | return 2 * atan2(x, w), pitch, 0 32 | roll = atan2(2*(w*x + y*z), w*w - x*x - y*y + z*z) 33 | #roll = atan2(2*(w*x + y*z), 1 - 2*(x*x + y*y)) 34 | hdg = atan2(2*(w*z + x*y), w*w + x*x - y*y - z*z) 35 | #hdg = atan2(2*(w*z + x*y), 1 - 2 *(y*y + z*z)) 36 | return hdg, pitch, roll 37 | 38 | 39 | class Quaternion: 40 | 41 | def __init__(self, w=1, x=0, y=0, z=0): # Default: the identity quaternion 42 | self.d = array('f', (w, x, y, z)) 43 | 44 | @property 45 | def w(self): 46 | return self[0] 47 | 48 | @w.setter 49 | def w(self, v): 50 | self[0] = v 51 | 52 | @property 53 | def x(self): 54 | return self[1] 55 | 56 | @x.setter 57 | def x(self, v): 58 | self[1] = v 59 | 60 | @property 61 | def y(self): 62 | return self[2] 63 | 64 | @y.setter 65 | def y(self, v): 66 | self[2] = v 67 | 68 | @property 69 | def z(self): 70 | return self[3] 71 | 72 | @z.setter 73 | def z(self, v): 74 | self[3] = v 75 | 76 | def normalise(self): 77 | if self[0] == 1: # acos(1) == 0. Identity quaternion: no rotation 78 | return Quaternion(1, 0, 0, 0) 79 | m = abs(self) # Magnitude 80 | assert m > 0.1 # rotation quaternion should have magnitude ~= 1 81 | if isclose(m, 1.0, rel_tol=mdelta): 82 | return self # No normalisation necessary 83 | return Quaternion(*(a/m for a in self)) 84 | 85 | def __getitem__(self, key): 86 | return self.d[key] 87 | 88 | def __setitem__(self, key, v): 89 | try: 90 | v1 = array('f', v) 91 | except TypeError: # Scalar 92 | v1 = v 93 | self.d[key] = v1 94 | 95 | def copy(self): 96 | return Quaternion(*self) 97 | 98 | def __abs__(self): # Return magnitude 99 | return sqrt(sum((d*d for d in self))) 100 | 101 | def __len__(self): 102 | return 4 103 | # Comparison: == and != perform equality test of all elements 104 | def __eq__(self, other): 105 | return all((isclose(a, b, rel_tol=mdelta, abs_tol=adelta) for a, b in zip(self, other))) 106 | 107 | def __ne__(self, other): 108 | return not self == other 109 | 110 | # < and > comparisons compare magnitudes. 111 | def __gt__(self, other): 112 | return abs(self) > abs(other) 113 | 114 | def __lt__(self, other): 115 | return abs(self) < abs(other) 116 | 117 | # <= and >= return True for complete equality otherwise magnitudes are compared. 118 | def __ge__(self, other): 119 | return True if self == other else abs(self) > abs(other) 120 | 121 | def __le__(self, other): 122 | return True if self == other else abs(self) < abs(other) 123 | 124 | def to_angle_axis(self): 125 | q = self.normalise() 126 | if isclose(q[0], 1.0, rel_tol = mdelta): 127 | return 0, 1, 0, 0 128 | theta = 2*acos(q[0]) 129 | s = sin(theta/2) 130 | return [theta] + [a/s for a in q[1:]] 131 | 132 | def conjugate(self): 133 | return Quaternion(self[0], *(-a for a in self[1:])) 134 | 135 | def inverse(self): # Reciprocal 136 | return self.conjugate()/sum((d*d for d in self)) 137 | 138 | def __str__(self): 139 | return 'w = {:4.2f} x = {:4.2f} y = {:4.2f} z = {:4.2f}'.format(*self) 140 | 141 | def __pos__(self): 142 | return Quaternion(*self) 143 | 144 | def __neg__(self): 145 | return Quaternion(*(-a for a in self)) 146 | 147 | def __truediv__(self, scalar): 148 | if isinstance(scalar, Quaternion): # See docs for reason 149 | raise ValueError('Cannot divide by Quaternion') 150 | return Quaternion(*(a/scalar for a in self)) 151 | 152 | def __rtruediv__(self, other): 153 | return self.inverse() * other 154 | 155 | # Multiply by quaternion, list, tuple, or scalar: result = self * other 156 | def __mul__(self, other): 157 | if isinstance(other, Quaternion): 158 | w1, x1, y1, z1 = self 159 | w2, x2, y2, z2 = other 160 | w = w1*w2 - x1*x2 - y1*y2 - z1*z2 161 | x = w1*x2 + x1*w2 + y1*z2 - z1*y2 162 | y = w1*y2 - x1*z2 + y1*w2 + z1*x2 163 | z = w1*z2 + x1*y2 - y1*x2 + z1*w2 164 | return Quaternion(w, x, y, z) 165 | length = _arglen(other) 166 | if length == 0: # Assume other is scalar 167 | return Quaternion(*(a * other for a in self)) 168 | elif length == 3: 169 | return Quaternion(0, *(a * b for a, b in zip(self[1:], other))) 170 | # length == 4: 171 | return Quaternion(*(a * b for a, b in zip(self, other))) 172 | 173 | def __rmul__(self, other): 174 | return self * other # Multiplication by scalars and tuples is commutative 175 | 176 | def __add__(self, other): 177 | if isinstance(other, Quaternion): 178 | return Quaternion(*(a + b for a, b in zip(self, other))) 179 | length = _arglen(other) 180 | if length == 0: # Assume other is scalar 181 | return Quaternion(self[0] + other, *self[1:]) # ? Is adding a scalar meaningful? 182 | elif length == 3: 183 | return Quaternion(0, *(a + b for a, b in zip(self[1:], other))) 184 | # length == 4: 185 | return Quaternion(*(a + b for a, b in zip(self, other))) 186 | 187 | def __radd__(self, other): 188 | return self.__add__(other) 189 | 190 | def __sub__(self, other): 191 | if isinstance(other, Quaternion): 192 | return Quaternion(*(a - b for a, b in zip(self, other))) 193 | length = _arglen(other) 194 | if length == 0: # Assume other is scalar 195 | return Quaternion(self[0] - other, *self[1:]) # ? Is this meaningful? 196 | elif length == 3: 197 | return Quaternion(0, *(a - b for a, b in zip(self[1:], other))) 198 | # length == 4: 199 | return Quaternion(*(a - b for a, b in zip(self, other))) 200 | 201 | def __rsub__(self, other): 202 | return other + self.__neg__() # via __radd__ 203 | 204 | def isrot(self): 205 | return isclose(abs(self), 1.0, rel_tol = mdelta) 206 | 207 | def isvec(self): 208 | return isclose(self[0], 0, abs_tol = adelta) 209 | 210 | def __matmul__(self, rot): 211 | return rot * self * rot.conjugate() 212 | 213 | def rrot(self, rot): 214 | return rot.conjugate() * self * rot 215 | 216 | # A vector quaternion has real part 0. It can represent a point in space. 217 | def Vector(x, y, z): 218 | return Quaternion(0, x, y, z) 219 | 220 | Point = Vector 221 | 222 | # A rotation quaternion is a unit quaternion i.e. magnitude == 1 223 | def Rotator(theta=0, x=0, y=0, z=0): 224 | s = sin(theta/2) 225 | m = sqrt(x*x + y*y + z*z) # Convert to unit vector 226 | if m > 0: 227 | return Quaternion(cos(theta/2), s*x/m, s*y/m, s*z/m) 228 | else: 229 | return Quaternion(1, 0, 0, 0) # Identity quaternion 230 | 231 | def Euler(heading, pitch, roll): 232 | cy = cos(heading * 0.5); 233 | sy = sin(heading * 0.5); 234 | cp = cos(pitch * 0.5); 235 | sp = sin(pitch * 0.5); 236 | cr = cos(roll * 0.5); 237 | sr = sin(roll * 0.5); 238 | 239 | w = cr * cp * cy + sr * sp * sy; 240 | x = sr * cp * cy - cr * sp * sy; 241 | y = cr * sp * cy + sr * cp * sy; 242 | z = cr * cp * sy - sr * sp * cy; 243 | return Quaternion(w, x, y, z) # Tait-Bryan angles but z == towards sky 244 | -------------------------------------------------------------------------------- /quaternion/quat_test.py: -------------------------------------------------------------------------------- 1 | # quat_test.py Test for quat.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | from math import sin, cos, isclose, pi, sqrt 7 | from quat import * 8 | 9 | print('Properties') 10 | q1 = Quaternion(1, 2, 3, 4) 11 | q1.w = 5 12 | q1.x = 6 13 | q1.y = 7 14 | q1.z = 8 15 | assert q1 == Quaternion(5, 6, 7, 8) 16 | assert (q1.w, q1.x, q1.y, q1.z) == (5, 6, 7, 8) 17 | 18 | # Numpy demo at https://quaternion.readthedocs.io/en/latest/README.html 19 | print('Hamilton product') 20 | q1 = Quaternion(1, 2, 3, 4) 21 | q2 = Quaternion(5, 6, 7, 8) 22 | q3 = Quaternion(-60, 12, 30, 24) 23 | assert q3 == q1 * q2 24 | 25 | print('Iterator protocol') 26 | assert Quaternion(*q1) == q1 27 | assert list(q1[1:]) == [2,3,4] 28 | foo = iter(q1) 29 | assert next(foo) == 1 30 | 31 | print('Assign from tuple') 32 | q1[1:] = (9, 10, 11) 33 | assert list(q1[1:]) == [9, 10, 11] 34 | q1[:] = (8, 9, 10, 99) 35 | assert list(q1[:]) == [8, 9, 10, 99] 36 | 37 | print('Assign from scalar') 38 | q1[0] = 88 39 | assert list(q1[:]) == [88, 9, 10, 99] 40 | 41 | print('Negation') 42 | q1 = Quaternion(1, 2, 3, 4) 43 | q2 = Quaternion(-1, -2, -3, -4) 44 | assert -q1 == q2 45 | 46 | print('Comparison operators and unary +') 47 | assert (q1 is +q1) == False 48 | assert q1 == +q1 49 | assert (q1 is q1.copy()) == False 50 | assert q1 == q1.copy() 51 | assert q1 >= q1.copy() 52 | assert q1 <= q1.copy() 53 | assert (q1 < q1.copy()) == False 54 | assert (q1 > q1.copy()) == False 55 | 56 | q2 = Quaternion(1, 2.1, 3, 4) 57 | assert q2 > q1 58 | assert q1 < q2 59 | assert q2 >= q1 60 | assert q1 <= q2 61 | assert (q1 == q2) == False 62 | assert q1 != q2 63 | 64 | print('Scalar add') 65 | q2 = Quaternion(5, 2, 3, 4) 66 | assert q2 == q1 + 4 67 | 68 | print('Scalar subtract') 69 | q2 = Quaternion(-3, 2, 3, 4) 70 | assert q2 == q1 - 4 71 | 72 | print('Scalar multiply') 73 | q2 = Quaternion(2, 4, 6, 8) 74 | assert q2 == q1 * 2 75 | 76 | print('Scalar divide') 77 | q2 = Quaternion(0.5, 1, 1.5, 2) 78 | assert q2 == q1/2 79 | 80 | print('Conjugate') 81 | assert q1.conjugate() == Quaternion(1, -2, -3, -4) 82 | 83 | print('Inverse') 84 | assert q1.inverse() * q1 == Quaternion(1, 0, 0, 0) 85 | 86 | print('Multiply by tuple') 87 | assert q1*(2,3,4) == Quaternion(0, 4, 9, 16) 88 | assert q1*(4,5,6,7) == Quaternion(4, 10, 18, 28) 89 | 90 | print('Add tuple') 91 | assert q1 + (2,3,4) == Quaternion(0, 4, 6, 8) 92 | assert q1 + (4,5,6,7) == Quaternion(5, 7, 9, 11) 93 | 94 | print('abs(), len(), str()') 95 | assert abs(Quaternion(2,2,2,2)) == 4 96 | assert len(q1) == 4 97 | assert str(q1) == 'w = 1.00 x = 2.00 y = 3.00 z = 4.00' 98 | 99 | print('Rotation') 100 | p = Vector(0, 1, 0) 101 | r = Rotator(pi/4, 0, 0, 1) 102 | rv = p @ r # Anticlockwise about z axis 103 | assert isclose(rv.w, 0, abs_tol=mdelta) 104 | assert isclose(rv.x, -sin(pi/4), rel_tol=mdelta) 105 | assert isclose(rv.y, sin(pi/4), rel_tol=mdelta) 106 | assert isclose(rv.z, 0, abs_tol=mdelta) 107 | 108 | 109 | p = Vector(1, 0, 0) 110 | r = Rotator(-pi/4, 0, 0, 1) 111 | rv = p @ r # Clockwise about z axis 112 | assert isclose(rv.w, 0, abs_tol=mdelta) 113 | assert isclose(rv.x, sin(pi/4), rel_tol=mdelta) 114 | assert isclose(rv.y, -sin(pi/4), rel_tol=mdelta) 115 | assert isclose(rv.z, 0, abs_tol=mdelta) 116 | 117 | p = Vector(0, 1, 0) 118 | r = Rotator(-pi/4, 1, 0, 0) 119 | rv = p @ r # Clockwise about x axis 120 | assert isclose(rv.w, 0, abs_tol=mdelta) 121 | assert isclose(rv.x, 0, abs_tol=mdelta) 122 | assert isclose(rv.y, sin(pi/4), rel_tol=mdelta) 123 | assert isclose(rv.z, -sin(pi/4), rel_tol=mdelta) 124 | 125 | print('Rotation using Euler angles') 126 | # Tait-Brian angles DIN9300: I thought z axis is down towards ground. 127 | # However https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles 128 | # and this implementation implies z is towards sky. 129 | # https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible 130 | 131 | # Test heading 132 | # Yaw/Heading: a +ve value is counter clockwise 133 | p = Vector(1, 0, 0) # x is direction of motion 134 | r = Euler(pi/4, 0, 0) # Heading 45°. 135 | rv = p @ r 136 | assert isclose(rv.w, 0, abs_tol=mdelta) 137 | assert isclose(rv.x, sin(pi/4), rel_tol=mdelta) 138 | assert isclose(rv.y, sin(pi/4), rel_tol=mdelta) 139 | assert isclose(rv.z, 0, abs_tol=mdelta) 140 | 141 | # Test pitch 142 | # A +ve value is aircraft nose down i.e. z +ve 143 | p = Vector(1, 0, 0) # x is direction of motion 144 | r = Euler(0, pi/4, 0) # Pitch 45°. 145 | rv = p @ r 146 | assert isclose(rv.w, 0, abs_tol=mdelta) 147 | assert isclose(rv.x, sin(pi/4), rel_tol=mdelta) 148 | assert isclose(rv.y, 0, abs_tol=mdelta) 149 | assert isclose(rv.z, -sin(pi/4), rel_tol=mdelta) # Implies z is towards sky 150 | 151 | # Test roll 152 | # A +ve value is y +ve 153 | p = Vector(0, 1, 0) # x is direction of motion. Vector is aircraft wing 154 | r = Euler(0, 0, pi/4) # Roll 45°. 155 | rv = p @ r 156 | assert isclose(rv.w, 0, abs_tol=mdelta) 157 | assert isclose(rv.x, 0, abs_tol=mdelta) 158 | assert isclose(rv.y, sin(pi/4), rel_tol=mdelta) 159 | assert isclose(rv.z, sin(pi/4), rel_tol=mdelta) # Implies z is towards sky 160 | 161 | print('euler() test') 162 | r = Euler(pi/4, 0, 0) 163 | assert isclose(euler(r)[0], pi/4, rel_tol=mdelta) 164 | r = Euler(0, pi/4, 0) 165 | assert isclose(euler(r)[1], pi/4, rel_tol=mdelta) 166 | r = Euler(0, 0, pi/4) 167 | assert isclose(euler(r)[2], pi/4, rel_tol=mdelta) 168 | 169 | print('isrot() and isvec()') 170 | assert Quaternion(0, 1, 2, 3).isvec() 171 | assert not Quaternion(0, 1, 2, 3).isrot() 172 | assert not Quaternion(1, 2, 3, 4).isvec() 173 | q = Rotator(1, 1, 1, 1) 174 | assert q.isrot() 175 | 176 | print('to_angle_axis()') 177 | t = Rotator(1, 1, 1, 1).to_angle_axis() 178 | assert isclose(t[0], 1, rel_tol=mdelta) 179 | for v in t[1:]: 180 | assert isclose(v, sqrt(1/3), rel_tol=mdelta) 181 | 182 | s = ''' 183 | *** Standard tests PASSED. *** 184 | 185 | The following test of reflected arithmetic operators will fail unless the 186 | firmware was compiled with MICROPY_PY_REVERSE_SPECIAL_METHODS. 187 | Runs on the Unix build.''' 188 | print(s) 189 | 190 | q1 = Quaternion(1, 2, 3, 4) 191 | assert 10 + Quaternion(1, 2, 3, 4) == Quaternion(11, 2, 3, 4) 192 | assert 1/q1 == q1.inverse() 193 | assert 2 * q1 == q1 + q1 194 | assert 1 - q1 == -q1 + 1 195 | 196 | s = ''' 197 | Reverse/reflected operators OK. 198 | 199 | *** All tests PASSED. *** 200 | ''' 201 | print(s) 202 | -------------------------------------------------------------------------------- /quaternion/setup3d.py: -------------------------------------------------------------------------------- 1 | # setup3d.py 2 | # Hardware specific setup for 3D demos 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2020 Peter Hinch 6 | 7 | from machine import I2C, SPI, Pin 8 | import gc 9 | 10 | # This module must export the following functions 11 | # fill(color) Fill the buffer with a color 12 | # line(xs, ys, xe, ye, color) Draw a line to the buffer 13 | # show() Display result 14 | # Also dimension bound variable. 15 | 16 | from ssd1351_16bit import SSD1351 as SSD 17 | _HEIGHT = const(128) # SSD1351 variant in use 18 | 19 | # IMU driver for test_imu only. With other IMU's the fusion module 20 | # may be used for quaternion output. 21 | # https://github.com/micropython-IMU/micropython-fusion 22 | from bno055 import BNO055 23 | # Initialise IMU 24 | i2c = I2C(2) 25 | imu = BNO055(i2c) 26 | 27 | # Export color constants 28 | WHITE = SSD.rgb(255, 255, 255) 29 | GREY = SSD.rgb(100, 100, 100) 30 | GREEN = SSD.rgb(0, 255, 0) 31 | BLUE = SSD.rgb(0, 0, 255) 32 | RED = SSD.rgb(255, 0, 0) 33 | YELLOW = SSD.rgb(255, 255, 0) 34 | CYAN = SSD.rgb(0, 255, 255) 35 | 36 | 37 | # Initialise display 38 | # Monkey patch size of square viewing area. No. of pixels for a change of 1.0 39 | # Viewing area is 128*128 40 | DIMENSION = 64 41 | 42 | gc.collect() 43 | _pdc = Pin('X1', Pin.OUT_PP, value=0) # Pins are for Pyboard 44 | _pcs = Pin('X2', Pin.OUT_PP, value=1) 45 | _prst = Pin('X3', Pin.OUT_PP, value=1) 46 | _spi = SPI(2) # scl Y9 sda Y10 47 | _ssd = SSD(_spi, _pcs, _pdc, _prst, height=_HEIGHT) # Create a display instance 48 | 49 | line = _ssd.line 50 | fill = _ssd.fill 51 | show = _ssd.show 52 | 53 | def setup(): 54 | return _ssd 55 | -------------------------------------------------------------------------------- /quaternion/setup3d_lcd160cr.py: -------------------------------------------------------------------------------- 1 | # setup3d_lcd160cr.py 2 | # Hardware specific setup for 3D demos 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2020 Peter Hinch 6 | 7 | from machine import I2C, SPI, Pin 8 | import framebuf 9 | import gc 10 | from lcd160cr import LCD160CR as SSD 11 | from lcd160cr import LANDSCAPE 12 | # IMU driver for test_imu only. With other IMU's the fusion module 13 | # may be used for quaternion output. 14 | # https://github.com/micropython-IMU/micropython-fusion 15 | #from bno055 import BNO055 16 | ## Initialise IMU 17 | #i2c = I2C(1) 18 | #imu = BNO055(i2c) 19 | 20 | # Export color constants 21 | WHITE = SSD.rgb(255, 255, 255) 22 | GREY = SSD.rgb(100, 100, 100) 23 | GREEN = SSD.rgb(0, 255, 0) 24 | BLUE = SSD.rgb(0, 0, 255) 25 | RED = SSD.rgb(255, 0, 0) 26 | YELLOW = SSD.rgb(255, 255, 0) 27 | CYAN = SSD.rgb(0, 255, 255) 28 | BLACK = SSD.rgb(0, 0, 0) 29 | 30 | # DIMENSION No. of pixels for a change of 1.0 31 | # Viewing area is 128*128 32 | DIMENSION = 64 33 | 34 | gc.collect() 35 | _buf = bytearray(160*128*2) # 40KiB 36 | _fb = framebuf.FrameBuffer(_buf, 160, 128, framebuf.RGB565) 37 | _lcd = SSD('Y') 38 | _lcd.set_orient(LANDSCAPE) 39 | _lcd.set_spi_win(0, 0, 160, 128) 40 | # Standard functions 41 | line = _fb.line 42 | fill = _fb.fill 43 | 44 | def show(): 45 | gc.collect() 46 | _lcd.show_framebuf(_fb) 47 | 48 | def setup(): 49 | return _lcd 50 | -------------------------------------------------------------------------------- /quaternion/test3d.py: -------------------------------------------------------------------------------- 1 | # test3d.py 3D objects created using quaternions 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | from math import pi 7 | import gc 8 | from quat import Rotator 9 | import graph3d as g3d 10 | from setup3d import * # Hardware setup and colors 11 | 12 | # Dict of display objects, each comprises a Shape 13 | dobj = g3d.DisplayDict(setup(), pi/6, 5) # Camera angle and z distance 14 | 15 | dobj['axes'] = g3d.Axes(WHITE) # Draw axes 16 | 17 | def demo(): 18 | dobj['cone'] = g3d.Cone(GREEN) * 0.7 19 | # Draw rectangle to check camera perspective 20 | square = (g3d.Square(YELLOW) -(0.5, 0.5, 0)) * 1.3 21 | rot = Rotator(pi/12, 1, 0, 0) 22 | dobj['perspective'] = square 23 | for _ in range(24): 24 | dobj['perspective'] @= rot 25 | dobj.show() 26 | rot = Rotator(pi/12, 0, 1, 0) 27 | for _ in range(24): 28 | dobj['perspective'] @= rot 29 | dobj.show() 30 | rot = Rotator(pi/12, 0, 0, 1) 31 | for _ in range(24): 32 | dobj['perspective'] @= rot 33 | dobj.show() 34 | dobj['perspective'] = g3d.Circle(RED) * 0.7 @ Rotator(pi/24, 0, 0, 1) # (1, 1, 0.5) for ellipse 35 | for _ in range(24): 36 | dobj['perspective'] @= rot 37 | dobj.show() 38 | del dobj['cone'] 39 | del dobj['perspective'] 40 | gc.collect() 41 | print('RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc())) 42 | dobj['perspective'] = g3d.Sphere(CYAN) * 0.5 - (0.5, 0.5, 0) 43 | rot = Rotator(pi/96, 1, 0, 0) 44 | gc.collect() 45 | print('RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc())) 46 | for _ in range(20): 47 | dobj['perspective'] += (0.025, 0.025, 0) # @= rot 48 | dobj.show() 49 | del dobj['perspective'] 50 | 51 | demo() 52 | -------------------------------------------------------------------------------- /quaternion/test_imu.py: -------------------------------------------------------------------------------- 1 | # test_imu.py Demo of cube being rotated by IMU data. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | from math import pi 7 | from time import sleep 8 | import gc 9 | from quat import Rotator, euler 10 | import graph3d as g3d 11 | from setup3d import * # Hardware setup and colors 12 | 13 | 14 | def imu_demo(): 15 | # Dict of display objects, each comprises a Shape 16 | dobj = g3d.DisplayDict(setup(), pi/6, 5) # Camera angle and z distance 17 | dobj['axes'] = g3d.Axes(WHITE) # Draw axes 18 | # Create a reference cube. Don't display. 19 | cube = g3d.Cube(RED, BLUE, GREEN) * (0.8, 0.8, 0.8) - (0.4, 0.4, 0.4) 20 | imuquat = Rotator() 21 | x = 0 22 | while True: 23 | dobj.show() 24 | sleep(0.1) 25 | imuquat[:] = imu.quaternion() # Assign IMU data to Quaternion instance 26 | # imuquat.normalise() No need: BNo055 data is a rotation quaternion 27 | dobj['cube'] = cube @ imuquat 28 | x += 1 29 | if x == 10: 30 | x = 0 31 | print('Quat heading {0:5.2f} roll {2:5.2f} pitch {1:5.2f}'.format(*euler(imuquat))) 32 | print('IMU heading {:5.2f} roll {:5.2f} pitch {:5.2f}'.format(*[x * pi/180 for x in imu.euler()])) 33 | gc.collect() 34 | print(gc.mem_free(), gc.mem_alloc()) 35 | del dobj['cube'] 36 | 37 | imu_demo() 38 | 39 | -------------------------------------------------------------------------------- /random/cheap_rand.py: -------------------------------------------------------------------------------- 1 | # pseudorandom numbers for MicroPython. ISR friendly version. 2 | # Probably poor quality numbers but useful in test scripts 3 | # Based on xorshift32 here https://en.wikipedia.org/wiki/Xorshift 4 | 5 | # Author: Peter Hinch 6 | # Copyright Peter Hinch 2020 Released under the MIT license 7 | 8 | # Example usage to produce numbers between 0 and 99 9 | # rand = cheap_rand(100) 10 | # successive calls to rand() will produce the required result. 11 | 12 | def cheap_rand(modulo, seed=0x3fba2): 13 | x = seed 14 | def func(): 15 | nonlocal x 16 | x ^= (x & 0x1ffff) << 13; 17 | x ^= x >> 17; 18 | x ^= (x & 0x1ffffff) << 5; 19 | return x % modulo 20 | return func 21 | 22 | # The sum total of my statistical testing 23 | #import pyb, micropython, time 24 | #rand = cheap_rand(1000) 25 | #sum = 0 26 | #cnt = 0 27 | #def avg(n): 28 | #global sum, cnt 29 | #sum += n 30 | #cnt += 1 31 | #def cb(t): 32 | #n = rand() 33 | #micropython.schedule(avg, n) 34 | 35 | #t = pyb.Timer(1, freq=20, callback=cb) 36 | #while True: 37 | #time.sleep(1) 38 | #print(sum/cnt) 39 | -------------------------------------------------------------------------------- /random/random.py: -------------------------------------------------------------------------------- 1 | # pseudorandom numbers for MicroPython 2 | # Uses the xorshift64star algorithm https://en.wikipedia.org/wiki/Xorshift 3 | # Author: Peter Hinch 4 | # Copyright Peter Hinch 2016 Released under the MIT license 5 | 6 | # Example usage to produce numbers between 0 and 99 7 | # rando = xorshift64star(100) 8 | # successive calls to rando() will produce the required result. 9 | # Timing 109us (Pyboard 1.1), 191us (Pyboard lite), 1.264ms (ESP8266) 10 | 11 | 12 | def xorshift64star(modulo, seed = 0xf9ac6ba4): 13 | x = seed 14 | def func(): 15 | nonlocal x 16 | x ^= x >> 12 17 | x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 18 | x ^= x >> 27 19 | return (x * 0x2545F4914F6CDD1D) % modulo 20 | return func 21 | -------------------------------------------------------------------------------- /random/yasmarang.py: -------------------------------------------------------------------------------- 1 | # yasmarang pseudorandom number generator. 2 | # Source http://www.literatecode.com/yasmarang 3 | 4 | # Author: Peter Hinch 5 | # Copyright Peter Hinch 2020 Released under the MIT license 6 | 7 | def yasmarang(): 8 | pad = 0xeda4baba 9 | n = 69 10 | d = 233 11 | dat = 0 12 | def func(): 13 | nonlocal pad, n, d, dat 14 | pad = (pad + dat + d * n) & 0xffffffff 15 | pad = ((pad<<3) + (pad>>29)) & 0xffffffff 16 | n = pad | 2 17 | d = (d ^ ((pad<<31) + (pad>>1))) & 0xffffffff 18 | dat ^= ((pad & 0xff) ^ (d>>8) ^ 1) & 0xff 19 | return (pad^(d<<5)^(pad>>18)^(dat<<1)) & 0xffffffff 20 | return func 21 | 22 | # Test: produces same outcome as website. 23 | #ym = yasmarang() 24 | #for _ in range(20): 25 | #print(hex(ym())) 26 | -------------------------------------------------------------------------------- /resilient/application.py: -------------------------------------------------------------------------------- 1 | # application.py 2 | 3 | # Released under the MIT licence. 4 | # Copyright (C) Peter Hinch 2018 5 | 6 | # The App class emulates a user application intended to service a single 7 | # client. In this case we have four instances of the application servicing 8 | # clients with ID's 1-4. 9 | 10 | import uasyncio as asyncio 11 | loop = asyncio.get_event_loop(runq_len=32, waitq_len=32) 12 | import ujson 13 | import server 14 | 15 | class App(): 16 | def __init__(self, client_id): 17 | self.client_id = client_id 18 | self.data = [0, 0] # Exchange a 2-list with remote 19 | loop = asyncio.get_event_loop() 20 | loop.create_task(self.start(loop)) 21 | 22 | async def start(self, loop): 23 | print('Client {} Awaiting connection.'.format(self.client_id)) 24 | conn = None 25 | while conn is None: 26 | await asyncio.sleep_ms(100) 27 | conn = server.client_conn(self.client_id) 28 | loop.create_task(self.reader(conn)) 29 | loop.create_task(self.writer(conn)) 30 | 31 | async def reader(self, conn): 32 | print('Started reader') 33 | while True: 34 | # Attempt to read data: server times out if none arrives in timeout 35 | # period closing the Connection. .readline() pauses until the 36 | # connection is re-established. 37 | line = await conn.readline() 38 | self.data = ujson.loads(line) 39 | # Receives [restart count, uptime in secs] 40 | print('Got', self.data, 'from remote', self.client_id) 41 | 42 | # Send [approx application uptime in secs, received client uptime] 43 | async def writer(self, conn): 44 | print('Started writer') 45 | count = 0 46 | while True: 47 | self.data[0] = count 48 | count += 1 49 | print('Sent', self.data, 'to remote', self.client_id) 50 | print() 51 | # .write() behaves as per .readline() 52 | await conn.write('{}\n'.format(ujson.dumps(self.data))) 53 | await asyncio.sleep(5) 54 | 55 | 56 | clients = [App(n) for n in range(1, 5)] # Accept 4 clients with ID's 1-4 57 | try: 58 | loop.run_until_complete(server.run(timeout=1500)) 59 | except KeyboardInterrupt: 60 | print('Interrupted') 61 | finally: 62 | print('Closing sockets') 63 | for s in server.socks: 64 | s.close() 65 | -------------------------------------------------------------------------------- /resilient/client_id.py: -------------------------------------------------------------------------------- 1 | MY_ID = '2\n' 2 | #_SERVER = '192.168.0.35' # Laptop 3 | SERVER = '192.168.0.10' # Pi 4 | PORT = 8123 5 | -------------------------------------------------------------------------------- /resilient/client_w.py: -------------------------------------------------------------------------------- 1 | # client_w.py Demo of a resilient asynchronous full-duplex ESP8266 client 2 | 3 | # Released under the MIT licence. 4 | # Copyright (C) Peter Hinch 2018 5 | 6 | import usocket as socket 7 | import uasyncio as asyncio 8 | import ujson 9 | import network 10 | import utime 11 | from machine import Pin 12 | import primitives as asyn # Stripped-down asyn.py 13 | # Get local config. ID is string of form '1\n' 14 | from client_id import MY_ID, PORT, SERVER 15 | 16 | 17 | class Client(): 18 | def __init__(self, timeout, loop): 19 | self.timeout = timeout 20 | self.led = Pin(2, Pin.OUT, value = 1) 21 | self._sta_if = network.WLAN(network.STA_IF) 22 | self._sta_if.active(True) 23 | self.server = socket.getaddrinfo(SERVER, PORT)[0][-1] # server read 24 | self.evfail = asyn.Event(100) 25 | self.lock = asyn.Lock(100) # 100ms pause 26 | self.connects = 0 # Connect count 27 | self.sock = None 28 | loop.create_task(self._run(loop)) 29 | 30 | # Make an attempt to connect to WiFi. May not succeed. 31 | async def _connect(self, s): 32 | print('Connecting to WiFi') 33 | s.active(True) 34 | s.connect() # ESP8266 remembers connection. 35 | # Break out on fail or success. 36 | while s.status() == network.STAT_CONNECTING: 37 | await asyncio.sleep(1) 38 | t = utime.ticks_ms() 39 | print('Checking WiFi stability for {}ms'.format(2 * self.timeout)) 40 | # Timeout ensures stable WiFi and forces minimum outage duration 41 | while s.isconnected() and utime.ticks_diff(utime.ticks_ms(), t) < 2 * self.timeout: 42 | await asyncio.sleep(1) 43 | 44 | async def _run(self, loop): 45 | s = self._sta_if 46 | while True: 47 | while not s.isconnected(): # Try until stable for 2*server timeout 48 | await self._connect(s) 49 | print('WiFi OK') 50 | self.sock = socket.socket() 51 | try: 52 | self.sock.connect(self.server) 53 | self.sock.setblocking(False) 54 | await self.send(self.sock, MY_ID) # Can throw OSError 55 | except OSError: 56 | pass 57 | else: 58 | self.evfail.clear() 59 | loop.create_task(asyn.Cancellable(self.reader)()) 60 | loop.create_task(asyn.Cancellable(self.writer)()) 61 | loop.create_task(asyn.Cancellable(self._keepalive)()) 62 | await self.evfail # Pause until something goes wrong 63 | await asyn.Cancellable.cancel_all() 64 | self.close() # Close sockets 65 | print('Fail detected. Coros stopped, disconnecting.') 66 | s.disconnect() 67 | await asyncio.sleep(1) 68 | while s.isconnected(): 69 | await asyncio.sleep(1) 70 | 71 | @asyn.cancellable 72 | async def reader(self): 73 | c = self.connects # Count and transmit successful connects 74 | try: 75 | while True: 76 | r = await self.readline() # OSError on fail 77 | if c == self.connects: # If read succeeded 78 | self.connects += 1 # update connect count 79 | d = ujson.loads(r) 80 | print('Got data', d) 81 | except OSError: 82 | self.evfail.set() 83 | 84 | @asyn.cancellable 85 | async def writer(self): 86 | data = [0, 0] 87 | try: 88 | while True: 89 | data[0] = self.connects # Send connection count 90 | async with self.lock: 91 | await self.send(self.sock, '{}\n'.format(ujson.dumps(data))) 92 | print('Sent data', data) 93 | data[1] += 1 # Packet counter 94 | await asyncio.sleep(5) 95 | except OSError: 96 | self.evfail.set() 97 | 98 | @asyn.cancellable 99 | async def _keepalive(self): 100 | tim = self.timeout * 2 // 3 # Ensure >= 1 keepalives in server t/o 101 | try: 102 | while True: 103 | await asyncio.sleep_ms(tim) 104 | async with self.lock: 105 | await self.send(self.sock, '\n') 106 | except OSError: 107 | self.evfail.set() 108 | 109 | # Read a line from nonblocking socket: reads can return partial data which 110 | # are joined into a line. Blank lines are keepalive packets which reset 111 | # the timeout: readline() pauses until a complete line has been received. 112 | async def readline(self): 113 | line = b'' 114 | start = utime.ticks_ms() 115 | while True: 116 | if line.endswith(b'\n'): 117 | if len(line) > 1: 118 | return line 119 | line = b'' 120 | start = utime.ticks_ms() # Blank line is keepalive 121 | self.led(not self.led()) 122 | await asyncio.sleep_ms(100) # nonzero wait seems empirically necessary 123 | d = self.sock.readline() 124 | if d == b'': 125 | raise OSError 126 | if d is not None: 127 | line = b''.join((line, d)) 128 | if utime.ticks_diff(utime.ticks_ms(), start) > self.timeout: 129 | raise OSError 130 | 131 | async def send(self, s, d): # Write a line to either socket. 132 | start = utime.ticks_ms() 133 | while len(d): 134 | ns = s.send(d) # OSError if client fails 135 | d = d[ns:] # Possible partial write 136 | await asyncio.sleep_ms(100) 137 | if utime.ticks_diff(utime.ticks_ms(), start) > self.timeout: 138 | raise OSError 139 | 140 | def close(self): 141 | print('Closing sockets.') 142 | if isinstance(self.sock, socket.socket): 143 | self.sock.close() 144 | 145 | 146 | loop = asyncio.get_event_loop() 147 | client = Client(1500, loop) # Server timeout set by server side app: 1.5s 148 | try: 149 | loop.run_forever() 150 | finally: 151 | client.close() # Close sockets in case of ctrl-C or bug 152 | -------------------------------------------------------------------------------- /resilient/primitives.py: -------------------------------------------------------------------------------- 1 | # primitives.py A stripped-down verion of asyn.py with Lock and Event only. 2 | # Save RAM on ESP8266 3 | 4 | # Released under the MIT licence. 5 | # Copyright (C) Peter Hinch 2018 6 | 7 | import uasyncio as asyncio 8 | 9 | class Lock(): 10 | def __init__(self, delay_ms=0): 11 | self._locked = False 12 | self.delay_ms = delay_ms 13 | 14 | def locked(self): 15 | return self._locked 16 | 17 | async def __aenter__(self): 18 | await self.acquire() 19 | return self 20 | 21 | async def __aexit__(self, *args): 22 | self.release() 23 | await asyncio.sleep(0) 24 | 25 | async def acquire(self): 26 | while True: 27 | if self._locked: 28 | await asyncio.sleep_ms(self.delay_ms) 29 | else: 30 | self._locked = True 31 | break 32 | 33 | def release(self): 34 | if not self._locked: 35 | raise RuntimeError('Attempt to release a lock which has not been set') 36 | self._locked = False 37 | 38 | 39 | class Event(): 40 | def __init__(self, delay_ms=0): 41 | self.delay_ms = delay_ms 42 | self.clear() 43 | 44 | def clear(self): 45 | self._flag = False 46 | self._data = None 47 | 48 | def __await__(self): 49 | while not self._flag: 50 | await asyncio.sleep_ms(self.delay_ms) 51 | 52 | __iter__ = __await__ 53 | 54 | def is_set(self): 55 | return self._flag 56 | 57 | def set(self, data=None): 58 | self._flag = True 59 | self._data = data 60 | 61 | def value(self): 62 | return self._data 63 | 64 | 65 | class Barrier(): 66 | def __init__(self, participants, func=None, args=()): 67 | self._participants = participants 68 | self._func = func 69 | self._args = args 70 | self._reset(True) 71 | 72 | def __await__(self): 73 | self._update() 74 | if self._at_limit(): # All other threads are also at limit 75 | if self._func is not None: 76 | launch(self._func, self._args) 77 | self._reset(not self._down) # Toggle direction to release others 78 | return 79 | 80 | direction = self._down 81 | while True: # Wait until last waiting thread changes the direction 82 | if direction != self._down: 83 | return 84 | yield 85 | 86 | __iter__ = __await__ 87 | 88 | def trigger(self): 89 | self._update() 90 | if self._at_limit(): # All other threads are also at limit 91 | if self._func is not None: 92 | launch(self._func, self._args) 93 | self._reset(not self._down) # Toggle direction to release others 94 | 95 | def _reset(self, down): 96 | self._down = down 97 | self._count = self._participants if down else 0 98 | 99 | def busy(self): 100 | if self._down: 101 | done = self._count == self._participants 102 | else: 103 | done = self._count == 0 104 | return not done 105 | 106 | def _at_limit(self): # Has count reached up or down limit? 107 | limit = 0 if self._down else self._participants 108 | return self._count == limit 109 | 110 | def _update(self): 111 | self._count += -1 if self._down else 1 112 | if self._count < 0 or self._count > self._participants: 113 | raise ValueError('Too many tasks accessing Barrier') 114 | 115 | # Task Cancellation 116 | try: 117 | StopTask = asyncio.CancelledError # More descriptive name 118 | except AttributeError: 119 | raise OSError('asyn.py requires uasyncio V1.7.1 or above.') 120 | 121 | class TaskId(): 122 | def __init__(self, taskid): 123 | self.taskid = taskid 124 | 125 | def __call__(self): 126 | return self.taskid 127 | 128 | # Sleep coro breaks up a sleep into shorter intervals to ensure a rapid 129 | # response to StopTask exceptions 130 | async def sleep(t, granularity=100): # 100ms default 131 | if granularity <= 0: 132 | raise ValueError('sleep granularity must be > 0') 133 | t = int(t * 1000) # ms 134 | if t <= granularity: 135 | await asyncio.sleep_ms(t) 136 | else: 137 | n, rem = divmod(t, granularity) 138 | for _ in range(n): 139 | await asyncio.sleep_ms(granularity) 140 | await asyncio.sleep_ms(rem) 141 | 142 | 143 | class Cancellable(): 144 | task_no = 0 # Generated task ID, index of tasks dict 145 | tasks = {} # Value is [coro, group, barrier] indexed by integer task_no 146 | 147 | @classmethod 148 | def _cancel(cls, task_no): 149 | task = cls.tasks[task_no][0] 150 | asyncio.cancel(task) 151 | 152 | @classmethod 153 | async def cancel_all(cls, group=0, nowait=False): 154 | tokill = cls._get_task_nos(group) 155 | barrier = Barrier(len(tokill) + 1) # Include this task 156 | for task_no in tokill: 157 | cls.tasks[task_no][2] = barrier 158 | cls._cancel(task_no) 159 | if nowait: 160 | barrier.trigger() 161 | else: 162 | await barrier 163 | 164 | @classmethod 165 | def _is_running(cls, group=0): 166 | tasks = cls._get_task_nos(group) 167 | if tasks == []: 168 | return False 169 | for task_no in tasks: 170 | barrier = cls.tasks[task_no][2] 171 | if barrier is None: # Running, not yet cancelled 172 | return True 173 | if barrier.busy(): 174 | return True 175 | return False 176 | 177 | @classmethod 178 | def _get_task_nos(cls, group): # Return task nos in a group 179 | return [task_no for task_no in cls.tasks if cls.tasks[task_no][1] == group] 180 | 181 | @classmethod 182 | def _get_group(cls, task_no): # Return group given a task_no 183 | return cls.tasks[task_no][1] 184 | 185 | @classmethod 186 | def _stopped(cls, task_no): 187 | if task_no in cls.tasks: 188 | barrier = cls.tasks[task_no][2] 189 | if barrier is not None: # Cancellation in progress 190 | barrier.trigger() 191 | del cls.tasks[task_no] 192 | 193 | def __init__(self, gf, *args, group=0, **kwargs): 194 | task = gf(TaskId(Cancellable.task_no), *args, **kwargs) 195 | if task in self.tasks: 196 | raise ValueError('Task already exists.') 197 | self.tasks[Cancellable.task_no] = [task, group, None] 198 | self.task_no = Cancellable.task_no # For subclass 199 | Cancellable.task_no += 1 200 | self.task = task 201 | 202 | def __call__(self): 203 | return self.task 204 | 205 | def __await__(self): # Return any value returned by task. 206 | return (yield from self.task) 207 | 208 | __iter__ = __await__ 209 | 210 | 211 | # @cancellable decorator 212 | 213 | def cancellable(f): 214 | def new_gen(*args, **kwargs): 215 | if isinstance(args[0], TaskId): # Not a bound method 216 | task_id = args[0] 217 | g = f(*args[1:], **kwargs) 218 | else: # Task ID is args[1] if a bound method 219 | task_id = args[1] 220 | args = (args[0],) + args[2:] 221 | g = f(*args, **kwargs) 222 | try: 223 | res = await g 224 | return res 225 | finally: 226 | NamedTask._stopped(task_id) 227 | return new_gen 228 | 229 | class NamedTask(Cancellable): 230 | instances = {} 231 | 232 | @classmethod 233 | async def cancel(cls, name, nowait=True): 234 | if name in cls.instances: 235 | await cls.cancel_all(group=name, nowait=nowait) 236 | return True 237 | return False 238 | 239 | @classmethod 240 | def is_running(cls, name): 241 | return cls._is_running(group=name) 242 | 243 | @classmethod 244 | def _stopped(cls, task_id): # On completion remove it 245 | name = cls._get_group(task_id()) # Convert task_id to task_no 246 | if name in cls.instances: 247 | instance = cls.instances[name] 248 | barrier = instance.barrier 249 | if barrier is not None: 250 | barrier.trigger() 251 | del cls.instances[name] 252 | Cancellable._stopped(task_id()) 253 | 254 | def __init__(self, name, gf, *args, barrier=None, **kwargs): 255 | if name in self.instances: 256 | raise ValueError('Task name "{}" already exists.'.format(name)) 257 | super().__init__(gf, *args, group=name, **kwargs) 258 | self.barrier = barrier 259 | self.instances[name] = self 260 | -------------------------------------------------------------------------------- /resilient/server.py: -------------------------------------------------------------------------------- 1 | # server.py Minimal server. 2 | 3 | # Released under the MIT licence. 4 | # Copyright (C) Peter Hinch 2018 5 | 6 | # Maintains bidirectional full-duplex links between server applications and 7 | # multiple WiFi connected clients. Each application instance connects to its 8 | # designated client. Connections areresilient and recover from outages of WiFi 9 | # and of the connected endpoint. 10 | # This server and the server applications are assumed to reside on a device 11 | # with a wired interface. 12 | 13 | # Run under MicroPython Unix build. 14 | 15 | import usocket as socket 16 | import uasyncio as asyncio 17 | import utime 18 | import primitives as asyn 19 | from client_id import PORT 20 | 21 | # Global list of open sockets. Enables application to close any open sockets in 22 | # the event of error. 23 | socks = [] 24 | 25 | # Read a line from a nonblocking socket. Nonblocking reads and writes can 26 | # return partial data. 27 | # Timeout: client is deemed dead if this period elapses without receiving data. 28 | # This seems to be the only way to detect a WiFi failure, where the client does 29 | # not get the chance explicitly to close the sockets. 30 | # Note: on WiFi connected devices sleep_ms(0) produced unreliable results. 31 | async def readline(s, timeout): 32 | line = b'' 33 | start = utime.ticks_ms() 34 | while True: 35 | if line.endswith(b'\n'): 36 | if len(line) > 1: 37 | return line 38 | line = b'' 39 | start = utime.ticks_ms() # A blank line is just a keepalive 40 | await asyncio.sleep_ms(100) # See note above 41 | d = s.readline() 42 | if d == b'': 43 | raise OSError 44 | if d is not None: 45 | line = b''.join((line, d)) 46 | if utime.ticks_diff(utime.ticks_ms(), start) > timeout: 47 | raise OSError 48 | 49 | async def send(s, d, timeout): 50 | start = utime.ticks_ms() 51 | while len(d): 52 | ns = s.send(d) # OSError if client fails 53 | d = d[ns:] 54 | await asyncio.sleep_ms(100) # See note above 55 | if utime.ticks_diff(utime.ticks_ms(), start) > timeout: 56 | raise OSError 57 | 58 | # Return the connection for a client if it is connected (else None) 59 | def client_conn(client_id): 60 | try: 61 | c = Connection.conns[client_id] 62 | except KeyError: 63 | return 64 | if c.ok(): 65 | return c 66 | 67 | # API: application calls server.run() 68 | # Not using uasyncio.start_server because of https://github.com/micropython/micropython/issues/4290 69 | async def run(timeout, nconns=10, verbose=False): 70 | addr = socket.getaddrinfo('0.0.0.0', PORT, 0, socket.SOCK_STREAM)[0][-1] 71 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 73 | socks.append(s) 74 | s.bind(addr) 75 | s.listen(nconns) 76 | verbose and print('Awaiting connection.') 77 | while True: 78 | yield asyncio.IORead(s) # Register socket for polling 79 | conn, addr = s.accept() 80 | conn.setblocking(False) 81 | try: 82 | idstr = await readline(conn, timeout) 83 | verbose and print('Got connection from client', idstr) 84 | socks.append(conn) 85 | Connection.go(int(idstr), timeout, verbose, conn) 86 | except OSError: 87 | if conn is not None: 88 | conn.close() 89 | 90 | # A Connection persists even if client dies (minimise object creation). 91 | # If client dies Connection is closed: .close() flags this state by closing its 92 | # socket and setting .conn to None (.ok() == False). 93 | class Connection(): 94 | conns = {} # index: client_id. value: Connection instance 95 | @classmethod 96 | def go(cls, client_id, timeout, verbose, conn): 97 | if client_id not in cls.conns: # New client: instantiate Connection 98 | Connection(client_id, timeout, verbose) 99 | cls.conns[client_id].conn = conn 100 | 101 | def __init__(self, client_id, timeout, verbose): 102 | self.client_id = client_id 103 | self.timeout = timeout 104 | self.verbose = verbose 105 | Connection.conns[client_id] = self 106 | # Startup timeout: cancel startup if both sockets not created in time 107 | self.lock = asyn.Lock(100) 108 | self.conn = None # Socket 109 | loop = asyncio.get_event_loop() 110 | loop.create_task(self._keepalive()) 111 | 112 | def ok(self): 113 | return self.conn is not None 114 | 115 | async def _keepalive(self): 116 | to = self.timeout * 2 // 3 117 | while True: 118 | await self.write('\n') 119 | await asyncio.sleep_ms(to) 120 | 121 | async def readline(self): 122 | while True: 123 | if self.verbose and not self.ok(): 124 | print('Reader Client:', self.client_id, 'awaiting OK status') 125 | while not self.ok(): 126 | await asyncio.sleep_ms(100) 127 | self.verbose and print('Reader Client:', self.client_id, 'OK') 128 | try: 129 | line = await readline(self.conn, self.timeout) 130 | return line 131 | except (OSError, AttributeError): # AttributeError if ok status lost while waiting for lock 132 | self.verbose and print('Read client disconnected: closing connection.') 133 | self.close() 134 | 135 | async def write(self, buf): 136 | while True: 137 | if self.verbose and not self.ok(): 138 | print('Writer Client:', self.client_id, 'awaiting OK status') 139 | while not self.ok(): 140 | await asyncio.sleep_ms(100) 141 | self.verbose and print('Writer Client:', self.client_id, 'OK') 142 | try: 143 | async with self.lock: # >1 writing task? 144 | await send(self.conn, buf, self.timeout) # OSError on fail 145 | return 146 | except (OSError, AttributeError): 147 | self.verbose and print('Write client disconnected: closing connection.') 148 | self.close() 149 | 150 | def close(self): 151 | if self.conn is not None: 152 | if self.conn in socks: 153 | socks.remove(self.conn) 154 | self.conn.close() 155 | self.conn = None 156 | -------------------------------------------------------------------------------- /reverse/reverse.py: -------------------------------------------------------------------------------- 1 | @micropython.asm_thumb 2 | def reverse(r0, r1): # bytearray, len(bytearray) 3 | add(r4, r0, r1) 4 | sub(r4, 1) # end address 5 | label(LOOP) 6 | ldrb(r5, [r0, 0]) 7 | ldrb(r6, [r4, 0]) 8 | strb(r6, [r0, 0]) 9 | strb(r5, [r4, 0]) 10 | add(r0, 1) 11 | sub(r4, 1) 12 | cmp(r4, r0) 13 | bpl(LOOP) 14 | 15 | def test(): 16 | a = bytearray([0, 1, 2, 3]) # even length 17 | reverse(a, len(a)) 18 | print(a) 19 | a = bytearray([0, 1, 2, 3, 4]) # odd length 20 | reverse(a, len(a)) 21 | print(a) 22 | 23 | 24 | # Bit reverse an 8 bit value 25 | def rbit8(v): 26 | v = (v & 0x0f) << 4 | (v & 0xf0) >> 4 27 | v = (v & 0x33) << 2 | (v & 0xcc) >> 2 28 | return (v & 0x55) << 1 | (v & 0xaa) >> 1 29 | 30 | # Bit reverse a 16 bit value 31 | def rbit16(v): 32 | v = (v & 0x00ff) << 8 | (v & 0xff00) >> 8 33 | v = (v & 0x0f0f) << 4 | (v & 0xf0f0) >> 4 34 | v = (v & 0x3333) << 2 | (v & 0xcccc) >> 2 35 | return (v & 0x5555) << 1 | (v & 0xaaaa) >> 1 36 | 37 | # Bit reverse a 32 bit value 38 | def rbit32(v): 39 | v = (v & 0x0000ffff) << 16 | (v & 0xffff0000) >> 16 40 | v = (v & 0x00ff00ff) << 8 | (v & 0xff00ff00) >> 8 41 | v = (v & 0x0f0f0f0f) << 4 | (v & 0xf0f0f0f0) >> 4 42 | v = (v & 0x33333333) << 2 | (v & 0xcccccccc) >> 2 43 | return (v & 0x55555555) << 1 | (v & 0xaaaaaaaa) >> 1 44 | -------------------------------------------------------------------------------- /sequence/check_mid.py: -------------------------------------------------------------------------------- 1 | # check_mid.py Check a sequence of incrementing message ID's. 2 | 3 | # Released under the MIT licence. See LICENSE. 4 | # Copyright (C) Peter Hinch 2020 5 | 6 | # For use in test scripts: message ID's increment without bound rather 7 | # than modulo N. Assumes message ID's start with 0 or 1. 8 | 9 | # Missing and duplicate message counter. Handles out-of-order messages. 10 | # Out of order messages will initially be missing to arrive later. 11 | # The most recent n message ID's are therefore not checked. If a 12 | # message is missing after n have been received, it is assumed lost. 13 | 14 | class CheckMid: 15 | def __init__(self, buff=15): 16 | self._buff = buff 17 | self._mids = set() 18 | self._miss = 0 # Count missing message ID's 19 | self._dupe = 0 # Duplicates 20 | self._oord = 0 # Received out of order 21 | self.bcnt = 0 # Client reboot count. Running totals over reboots: 22 | self._tot_miss = 0 # Missing 23 | self._tot_dupe = 0 # Dupes 24 | self._tot_oord = 0 # Out of order 25 | 26 | @property 27 | def miss(self): 28 | return self._miss + self._tot_miss 29 | 30 | @property 31 | def dupe(self): 32 | return self._dupe + self._tot_dupe 33 | 34 | @property 35 | def oord(self): 36 | return self._oord + self._tot_oord 37 | 38 | def __call__(self, mid): 39 | mids = self._mids 40 | if mid <= 1 and len(mids) > 1: # Target has rebooted 41 | self._mids.clear() 42 | self._tot_miss += self._miss 43 | self._tot_dupe += self._dupe 44 | self._tot_oord += self._oord 45 | self._miss = 0 46 | self._dupe = 0 47 | self._oord = 0 48 | self.bcnt += 1 49 | if mid in mids: 50 | self._dupe += 1 51 | elif mids and mid < max(mids): 52 | self._oord += 1 53 | mids.add(mid) 54 | if len(mids) > self._buff: 55 | oldest = min(mids) 56 | mids.remove(oldest) 57 | self._miss += min(mids) - oldest - 1 58 | 59 | # Usage/demo 60 | #cm = CheckMid(5) 61 | #s1 = (1,2,3,4,5,8,9,10,11,12,13,17,17,16,18,19,20,21,22,23,24,29,28,27,26,30,31,32,33,34,35,36,1,2,3,4,5,6,7,8) 62 | #for x in s1: 63 | #cm(x) 64 | #print(cm.dupe, cm.miss, cm.oord, cm.bcnt) 65 | -------------------------------------------------------------------------------- /soft_wdt/soft_wdt.py: -------------------------------------------------------------------------------- 1 | # soft_wdt.py A software watchdog timer 2 | # Supports fixed or variable time period. 3 | # Supports temporary suspension and permanent cancellation. 4 | 5 | # Copyright (c) Peter Hinch 2019 6 | # Released under the MIT licence. 7 | 8 | from machine import Timer, reset 9 | from micropython import const 10 | WDT_SUSPEND = const(-1) 11 | WDT_CANCEL = const(-2) 12 | WDT_CB = const(-3) 13 | 14 | def wdt(secs=0): 15 | timer = Timer(-1) 16 | timer.init(period=1000, mode=Timer.PERIODIC, callback=lambda t:wdt_feed()) 17 | cnt = secs 18 | run = False # Disable until 1st feed 19 | def inner(feed=WDT_CB): 20 | nonlocal cnt, run, timer 21 | if feed > 0: # Call with variable timeout 22 | cnt = feed 23 | run = True 24 | elif feed == 0: # Fixed timeout 25 | cnt = secs 26 | run = True 27 | elif feed < 0: # WDT control/callback 28 | if feed == WDT_SUSPEND: 29 | run = False # Temporary suspension 30 | elif feed == WDT_CANCEL: 31 | timer.deinit() # Permanent cancellation 32 | elif feed == WDT_CB and run: # Timer callback and is running. 33 | cnt -= 1 34 | if cnt <= 0: 35 | reset() 36 | return inner 37 | 38 | wdt_feed = wdt(2) # Modify this for preferred default period (secs) 39 | -------------------------------------------------------------------------------- /soft_wdt/swdt_tests.py: -------------------------------------------------------------------------------- 1 | # swdt_tests Test/demo scripts for soft_wdt 2 | 3 | # Copyright (c) Peter Hinch 2019 4 | # Released under the MIT licence. 5 | import utime 6 | from soft_wdt import wdt_feed, WDT_CANCEL, WDT_SUSPEND 7 | 8 | # Exception trapping and cancellation are invaluable when debugging code: put 9 | # cancellation in the finally block of a try statement so that the hardware 10 | # doesn't reset when code terminates either naturally or in response to an 11 | # error or ctrl-c interrupt. 12 | 13 | # Normal operation. Illustrates exception trapping. You can interrupt this with 14 | # ctrl-c 15 | def normal(): 16 | try: 17 | for x in range(10, 0, -1): 18 | print('nunning', x) 19 | utime.sleep(0.5) 20 | wdt_feed(5) # Hold off for 5s 21 | 22 | print('Should reset in 5s') 23 | utime.sleep(10) 24 | except KeyboardInterrupt: 25 | pass 26 | finally: 27 | wdt_feed(WDT_CANCEL) # Should never execute 28 | 29 | # Suspend and resume 30 | 31 | def suspend(): 32 | for x in range(10, 0, -1): 33 | print('nunning', x) 34 | utime.sleep(0.5) 35 | wdt_feed(5) # Hold off for 5s 36 | 37 | wdt_feed(WDT_SUSPEND) 38 | for x in range(5, 0, -1): 39 | print('suspended', x) 40 | utime.sleep(0.5) 41 | 42 | for x in range(5, 0, -1): 43 | print('nunning', x) 44 | utime.sleep(0.5) 45 | wdt_feed(5) # Hold off for 5s 46 | 47 | print('Should reset in 5s') 48 | utime.sleep(10) 49 | wdt_feed(WDT_CANCEL) # Should never execute 50 | 51 | # Default period 52 | 53 | def default(): 54 | for x in range(10, 0, -1): 55 | print('nunning', x) 56 | utime.sleep(0.5) 57 | wdt_feed(5) # Hold off for 5s 58 | 59 | wdt_feed(0) # Use default period 60 | print('Should reset in 2s') 61 | utime.sleep(10) 62 | wdt_feed(WDT_CANCEL) # Should never execute 63 | 64 | # Cancellation 65 | def cancel(): 66 | for x in range(10, 0, -1): 67 | print('nunning', x) 68 | utime.sleep(0.5) 69 | wdt_feed(5) # Hold off for 5s 70 | 71 | wdt_feed(WDT_CANCEL) 72 | 73 | print('Pause 10s: should not reset in 5s') 74 | utime.sleep(10) 75 | print('WDT is permanently cancelled.') 76 | -------------------------------------------------------------------------------- /temp/read_2_hard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-samples/53ef595a5870063e611ee2273aa47faa0d33fcf2/temp/read_2_hard.jpg -------------------------------------------------------------------------------- /timed_function/timed_func.py: -------------------------------------------------------------------------------- 1 | # Time a function call by means of a decorator 2 | 3 | import utime 4 | 5 | # @timed_function 6 | # Print time taken by a function call 7 | 8 | def timed_function(f, *args, **kwargs): 9 | def new_func(*args, **kwargs): 10 | t = utime.ticks_us() 11 | result = f(*args, **kwargs) 12 | delta = utime.ticks_diff(utime.ticks_us(), t) 13 | print('Function {} Time = {:6.3f}ms'.format(f.__name__, delta/1000)) 14 | return result 15 | return new_func 16 | 17 | @timed_function 18 | def test(): 19 | utime.sleep_us(10000) 20 | 21 | # @time_acc_function 22 | # applied to a function causes it to print the number of times it was called 23 | # with the accumulated time used. 24 | 25 | def time_acc_function(f, *args, **kwargs): 26 | ncalls = 0 27 | ttime = 0.0 28 | def new_func(*args, **kwargs): 29 | nonlocal ncalls, ttime 30 | t = utime.ticks_us() 31 | result = f(*args, **kwargs) 32 | delta = utime.ticks_diff(utime.ticks_us(), t) 33 | ncalls += 1 34 | ttime += delta 35 | print('Function: {} Call count = {} Total time = {:6.3f}ms'.format(f.__name__, ncalls, ttime/1000)) 36 | return result 37 | return new_func 38 | 39 | -------------------------------------------------------------------------------- /timed_function/timeout.py: -------------------------------------------------------------------------------- 1 | # Implement a timeout using a closure 2 | import utime 3 | 4 | def to(t): 5 | tstart = utime.ticks_ms() 6 | def foo(): 7 | return utime.ticks_diff(utime.ticks_ms(), tstart) > t 8 | return foo 9 | 10 | # Usage 11 | t = to(3000) 12 | for _ in range(10): 13 | print(t()) 14 | utime.sleep(0.5) 15 | 16 | -------------------------------------------------------------------------------- /watchdog/wdog.py: -------------------------------------------------------------------------------- 1 | # Class for pybord watchdog timer 2 | import stm, pyb 3 | 4 | @micropython.asm_thumb 5 | def clz(r0): 6 | clz(r0, r0) # return no. of leading zeros in passed integer 7 | 8 | class wdog(object): 9 | def start(self, ms): 10 | assert ms <= 32768 and ms >= 1, "Time value must be from 1 to 32768mS" 11 | prescaler = 23 - clz(ms -1) 12 | div_value = ((ms << 3) -1) >> prescaler 13 | stm.mem16[stm.IWDG + stm.IWDG_KR] = 0x5555 14 | stm.mem16[stm.IWDG + stm.IWDG_PR] = (stm.mem16[stm.IWDG + stm.IWDG_PR] & 0xfff8) | prescaler 15 | stm.mem16[stm.IWDG + stm.IWDG_RLR] = (stm.mem16[stm.IWDG + stm.IWDG_RLR] & 0xf000) | div_value 16 | stm.mem16[stm.IWDG + stm.IWDG_KR] = 0xcccc 17 | def feed(self): 18 | stm.mem16[stm.IWDG + stm.IWDG_KR] = 0xaaaa 19 | 20 | def test(): 21 | led = pyb.LED(2) 22 | led1 = pyb.LED(3) 23 | dog = wdog() 24 | dog.start(1000) 25 | for x in range(10): 26 | led.toggle() 27 | pyb.delay(500) 28 | dog.feed() 29 | dog.start(4000) 30 | for x in range(20): 31 | led1.toggle() 32 | pyb.delay(500) 33 | --------------------------------------------------------------------------------