Skip to content

Commit 1b8ff4e

Browse files
authored
Merge pull request #80 from BiffoBear/dhcp_error_handling
DHCP state machine error handling
2 parents f97e78c + 6bc9f63 commit 1b8ff4e

File tree

4 files changed

+729
-67
lines changed

4 files changed

+729
-67
lines changed

adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py

+78-67
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
DHCP_HLENETHERNET = const(0x06)
6666
DHCP_HOPS = const(0x00)
6767

68-
MAGIC_COOKIE = const(0x63825363)
68+
MAGIC_COOKIE = b"c\x82Sc" # Four bytes 99.130.83.99
6969
MAX_DHCP_OPT = const(0x10)
7070

7171
# Default DHCP Server port
@@ -179,7 +179,7 @@ def send_dhcp_message(
179179
# Transaction ID (xid)
180180
self._initial_xid = htonl(self._transaction_id)
181181
self._initial_xid = self._initial_xid.to_bytes(4, "big")
182-
_BUFF[4:7] = self._initial_xid
182+
_BUFF[4:8] = self._initial_xid
183183

184184
# seconds elapsed
185185
_BUFF[8] = (int(time_elapsed) & 0xFF00) >> 8
@@ -195,18 +195,15 @@ def send_dhcp_message(
195195
# as they're already set to 0.0.0.0
196196
# Except when renewing, then fill in ciaddr
197197
if renew:
198-
_BUFF[12:15] = bytes(self.local_ip)
198+
_BUFF[12:16] = bytes(self.local_ip)
199199

200200
# chaddr
201201
_BUFF[28:34] = self._mac_address
202202

203203
# NOTE: 192 octets of 0's, BOOTP legacy
204204

205205
# Magic Cookie
206-
_BUFF[236] = (MAGIC_COOKIE >> 24) & 0xFF
207-
_BUFF[237] = (MAGIC_COOKIE >> 16) & 0xFF
208-
_BUFF[238] = (MAGIC_COOKIE >> 8) & 0xFF
209-
_BUFF[239] = MAGIC_COOKIE & 0xFF
206+
_BUFF[236:240] = MAGIC_COOKIE
210207

211208
# Option - DHCP Message Type
212209
_BUFF[240] = 53
@@ -262,10 +259,10 @@ def send_dhcp_message(
262259
# pylint: disable=too-many-branches, too-many-statements
263260
def parse_dhcp_response(
264261
self,
265-
) -> Union[Tuple[int, bytes], Tuple[int, int]]:
262+
) -> Tuple[int, bytearray]:
266263
"""Parse DHCP response from DHCP server.
267264
268-
:return Union[Tuple[int, bytes], Tuple[int, int]]: DHCP packet type.
265+
:return Tuple[int, bytearray]: DHCP packet type and ID.
269266
"""
270267
# store packet in buffer
271268
_BUFF = self._sock.recv()
@@ -281,16 +278,16 @@ def parse_dhcp_response(
281278
)
282279

283280
xid = _BUFF[4:8]
284-
if bytes(xid) < self._initial_xid:
285-
print("f")
286-
return 0, 0
281+
if bytes(xid) != self._initial_xid:
282+
raise ValueError("DHCP response ID mismatch.")
287283

288284
self.local_ip = tuple(_BUFF[16:20])
289-
if _BUFF[28:34] == 0:
290-
return 0, 0
285+
# Check that there is a server ID.
286+
if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00":
287+
raise ValueError("No DHCP server ID in the response.")
291288

292-
if int.from_bytes(_BUFF[235:240], "big") != MAGIC_COOKIE:
293-
return 0, 0
289+
if _BUFF[236:240] != MAGIC_COOKIE:
290+
raise ValueError("No DHCP Magic Cookie in the response.")
294291

295292
# -- Parse Packet, VARIABLE -- #
296293
ptr = 240
@@ -323,8 +320,8 @@ def parse_dhcp_response(
323320
ptr += 1
324321
opt_len = _BUFF[ptr]
325322
ptr += 1
326-
self.gateway_ip = tuple(_BUFF[ptr : ptr + opt_len])
327-
ptr += opt_len
323+
self.gateway_ip = tuple(_BUFF[ptr : ptr + 4])
324+
ptr += opt_len # still increment even though we only read 1 addr.
328325
elif _BUFF[ptr] == DNS_SERVERS:
329326
ptr += 1
330327
opt_len = _BUFF[ptr]
@@ -427,65 +424,79 @@ def _dhcp_state_machine(self) -> None:
427424
if self._sock.available():
428425
if self._debug:
429426
print("* DHCP: Parsing OFFER")
430-
msg_type, xid = self.parse_dhcp_response()
431-
if msg_type == DHCP_OFFER:
432-
# Check if transaction ID matches, otherwise it may be an offer
433-
# for another device
434-
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
435-
if self._debug:
436-
print(
437-
"* DHCP: Send request to {}".format(self.dhcp_server_ip)
427+
try:
428+
msg_type, xid = self.parse_dhcp_response()
429+
except ValueError as error:
430+
if self._debug:
431+
print(error)
432+
else:
433+
if msg_type == DHCP_OFFER:
434+
# Check if transaction ID matches, otherwise it may be an offer
435+
# for another device
436+
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
437+
if self._debug:
438+
print(
439+
"* DHCP: Send request to {}".format(
440+
self.dhcp_server_ip
441+
)
442+
)
443+
self._transaction_id = (
444+
self._transaction_id + 1
445+
) & 0x7FFFFFFF
446+
self.send_dhcp_message(
447+
DHCP_REQUEST, (time.monotonic() - self._start_time)
438448
)
439-
self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF
440-
self.send_dhcp_message(
441-
DHCP_REQUEST, (time.monotonic() - self._start_time)
442-
)
443-
self._dhcp_state = STATE_DHCP_REQUEST
449+
self._dhcp_state = STATE_DHCP_REQUEST
450+
else:
451+
if self._debug:
452+
print("* DHCP: Received OFFER with non-matching xid")
444453
else:
445454
if self._debug:
446-
print("* DHCP: Received OFFER with non-matching xid")
447-
else:
448-
if self._debug:
449-
print("* DHCP: Received DHCP Message is not OFFER")
455+
print("* DHCP: Received DHCP Message is not OFFER")
450456

451457
elif self._dhcp_state == STATE_DHCP_REQUEST:
452458
if self._sock.available():
453459
if self._debug:
454460
print("* DHCP: Parsing ACK")
455-
msg_type, xid = self.parse_dhcp_response()
456-
# Check if transaction ID matches, otherwise it may be
457-
# for another device
458-
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
459-
if msg_type == DHCP_ACK:
460-
if self._debug:
461-
print("* DHCP: Successful lease")
462-
self._sock.close()
463-
self._sock = None
464-
self._dhcp_state = STATE_DHCP_LEASED
465-
self._last_lease_time = self._start_time
466-
if self._lease_time == 0:
467-
self._lease_time = DEFAULT_LEASE_TIME
468-
if self._t1 == 0:
469-
# T1 is 50% of _lease_time
470-
self._t1 = self._lease_time >> 1
471-
if self._t2 == 0:
472-
# T2 is 87.5% of _lease_time
473-
self._t2 = self._lease_time - (self._lease_time >> 3)
474-
self._renew_in_sec = self._t1
475-
self._rebind_in_sec = self._t2
476-
self._eth.ifconfig = (
477-
self.local_ip,
478-
self.subnet_mask,
479-
self.gateway_ip,
480-
self.dns_server_ip,
481-
)
482-
gc.collect()
461+
try:
462+
msg_type, xid = self.parse_dhcp_response()
463+
except ValueError as error:
464+
if self._debug:
465+
print(error)
466+
else:
467+
# Check if transaction ID matches, otherwise it may be
468+
# for another device
469+
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
470+
if msg_type == DHCP_ACK:
471+
if self._debug:
472+
print("* DHCP: Successful lease")
473+
self._sock.close()
474+
self._sock = None
475+
self._dhcp_state = STATE_DHCP_LEASED
476+
self._last_lease_time = self._start_time
477+
if self._lease_time == 0:
478+
self._lease_time = DEFAULT_LEASE_TIME
479+
if self._t1 == 0:
480+
# T1 is 50% of _lease_time
481+
self._t1 = self._lease_time >> 1
482+
if self._t2 == 0:
483+
# T2 is 87.5% of _lease_time
484+
self._t2 = self._lease_time - (self._lease_time >> 3)
485+
self._renew_in_sec = self._t1
486+
self._rebind_in_sec = self._t2
487+
self._eth.ifconfig = (
488+
self.local_ip,
489+
self.subnet_mask,
490+
self.gateway_ip,
491+
self.dns_server_ip,
492+
)
493+
gc.collect()
494+
else:
495+
if self._debug:
496+
print("* DHCP: Received DHCP Message is not ACK")
483497
else:
484498
if self._debug:
485-
print("* DHCP: Received DHCP Message is not ACK")
486-
else:
487-
if self._debug:
488-
print("* DHCP: Received non-matching xid")
499+
print("* DHCP: Received non-matching xid")
489500

490501
elif self._dhcp_state == STATE_DHCP_WAIT:
491502
if time.monotonic() > (self._start_time + DHCP_WAIT_TIME):

examples/wiznet5k_wsgiservertest.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC
2+
#
3+
# SPDX-License-Identifier: MIT
4+
#
5+
# This demo was tested with the PoE-FeatherWing, which contains a 24AA02E48
6+
# chip to provide a globally unique MAC address, but can also work without
7+
# this chip for testing purposes by using a hard coded MAC.
8+
#
9+
# It also contains a `get_static_file` function that demonstrates how to
10+
# use a generator to serve large static files without using up too much
11+
# memory. To avoid having to put extra files in the repo, it just serves
12+
# `code.py` which isn't very large, but to properly test it, adjust the code
13+
# to serve an image of several 100 kB to see how it works.
14+
#
15+
# There's also an endpoint that demonstrates that `requests` can be used to
16+
# get data from another socket and serve it.
17+
#
18+
19+
import board
20+
import busio
21+
import digitalio
22+
import neopixel
23+
24+
import adafruit_requests as requests
25+
from adafruit_wsgi.wsgi_app import WSGIApp
26+
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
27+
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
28+
import adafruit_wiznet5k.adafruit_wiznet5k_wsgiserver as server
29+
30+
31+
print("Wiznet5k Web Server Test")
32+
33+
34+
def get_mac(i2c_obj):
35+
"Read MAC from 24AA02E48 chip and return it"
36+
mac_addr = bytearray(6)
37+
while not i2c_obj.try_lock():
38+
pass
39+
i2c_obj.writeto(0x50, bytearray((0xFA,)))
40+
i2c_obj.readfrom_into(0x50, mac_addr, start=0, end=6)
41+
i2c_obj.unlock()
42+
return mac_addr
43+
44+
45+
def get_static_file(filename):
46+
"Static file generator"
47+
with open(filename, "rb") as f:
48+
b = None
49+
while b is None or len(b) == 2048:
50+
b = f.read(2048)
51+
yield b
52+
53+
54+
# Status LED
55+
led = neopixel.NeoPixel(board.NEOPIXEL, 1)
56+
led.brightness = 0.3
57+
led[0] = (255, 0, 0)
58+
59+
# Chip Select for PoE-FeatherWing and Adafruit Ethernet FeatherWing
60+
cs = digitalio.DigitalInOut(board.D10)
61+
# Chip Select for Particle Ethernet FeatherWing
62+
# cs = digitalio.DigitalInOut(board.D5)
63+
64+
# Initialize SPI bus
65+
spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
66+
67+
try:
68+
# Initialize the I2C bus to read the MAC
69+
i2c = busio.I2C(board.SCL, board.SDA)
70+
# Read the MAC from the 24AA02E48 chip
71+
mac = get_mac(i2c)
72+
except (RuntimeError, OSError):
73+
# Hard coded MAC if there is no 24AA02E48
74+
mac = b"\xFE\xED\xDE\xAD\xBE\xEF"
75+
76+
# Initialize Ethernet interface with DHCP
77+
eth = WIZNET5K(spi_bus, cs, mac=mac)
78+
79+
# Initialize a requests object with a socket and ethernet interface
80+
requests.set_socket(socket, eth)
81+
82+
83+
# Here we create our application, registering the
84+
# following functions to be called on specific HTTP GET requests routes
85+
86+
web_app = WSGIApp()
87+
88+
89+
@web_app.route("/led/<r>/<g>/<b>")
90+
def led_on(request, r, g, b): # pylint: disable=unused-argument
91+
print("LED handler")
92+
led.fill((int(r), int(g), int(b)))
93+
return ("200 OK", [], ["LED set!"])
94+
95+
96+
@web_app.route("/")
97+
def root(request): # pylint: disable=unused-argument
98+
print("Root WSGI handler")
99+
return ("200 OK", [], ["Root document"])
100+
101+
102+
@web_app.route("/large")
103+
def large(request): # pylint: disable=unused-argument
104+
print("Large pattern handler")
105+
return ("200 OK", [], ["*-.-" * 2000])
106+
107+
108+
@web_app.route("/code")
109+
def code(request): # pylint: disable=unused-argument
110+
print("Static file code.py handler")
111+
return ("200 OK", [], get_static_file("code.py"))
112+
113+
114+
@web_app.route("/btc")
115+
def btc(request): # pylint: disable=unused-argument
116+
print("BTC handler")
117+
r = requests.get("http://api.coindesk.com/v1/bpi/currentprice/USD.json")
118+
result = r.text
119+
r.close()
120+
return ("200 OK", [], [result])
121+
122+
123+
# Here we setup our server, passing in our web_app as the application
124+
server.set_interface(eth)
125+
wsgiServer = server.WSGIServer(80, application=web_app)
126+
127+
print("Open this IP in your browser: ", eth.pretty_ip(eth.ip_address))
128+
129+
# Start the server
130+
wsgiServer.start()
131+
led[0] = (0, 0, 255)
132+
133+
while True:
134+
# Our main loop where we have the server poll for incoming requests
135+
wsgiServer.update_poll()
136+
# Maintain DHCP lease
137+
eth.maintain_dhcp_lease()
138+
# Could do any other background tasks here, like reading sensors

optional_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
22
#
33
# SPDX-License-Identifier: Unlicense
4+
pytest

0 commit comments

Comments
 (0)