├── 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 | {{i}} | {{"%2d" % i ** 2}} |
7 | {% endfor %}
8 |
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 | 
4 |
5 | 
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 | 
13 | Microwave oven.
14 | 
15 | Microwave waveforms.
16 |
17 | 
18 | Integration screen.
19 |
20 | 
21 | Soldering iron on 3KW range.
22 |
23 | 
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 |
--------------------------------------------------------------------------------