198ж. UDP sensor: broadcast, network discovery and a custom protocol
Sections 105 and 106 went through the network layer by layer, and 106а brought frames into F407 memory. Here is the first complete network device: once a second the sensor sends a measurement to the subnet broadcast address or to a multicast group, answers the question "who is here?" with its MAC, IP and version, and a program on the PC finds all devices on the network, keeps a log and notices lost packets by their numbers. The protocol is our own: a 16-byte header, the data and a CRC.
The device is written on lwIP (raw API, no OS) and built two ways: for the
F407 with armclang — for the sizes and the warnings — and under Linux
from the same sensor_app.c, where a raw socket takes the place of the F407 MAC. Three
such devices and a python3 collector live in one docker network.
Sources: lwIP 2.1.2 from the STM32CubeF4 1.28.0 package
(udp.c, udp.h, opt.h, timeouts.c), RM0090 (Ethernet descriptors),
stm32f4xx_hal_eth.h and stm32f4xx_ll_adc.h from the Keil STM32F4xx_DFP pack.
Files are in _verify/udp198h/: us_proto.h/.c (the packet), sensor_app.c
(the device), port_f407.c (F407), main_linux.c (Linux), collector.py
(the collector), and the saved runs *_host.txt.
Terms used in the documentation
Term Meaning broadcast address the address with all ones in the host part multicast group a group address in the range 224.0.0.0–239.255.255.255 IGMP join joining a group: the host tells the network it wants that group's packets raw API the lwIP callback interface: functions and handlers without an OS pbuf the lwIP packet buffer; can be a chain of several pieces discovery finding devices on the network sequence number the running number of a packet checksum offload the network adapter completes the checksum
198ж.1 What the device does
| Function | How |
|---|---|
| measurement | every 1000 ms: uptime, die temperature, supply voltage |
| where to | subnet broadcast address or group 239.19.8.1, port 19800 |
| discovery | answers WHO with HERE: MAC, IP, mask, version, period |
| losses | a number in every packet; a ring of the last 32 measurements; resend on GET |
| stack | lwIP 2.1.2, NO_SYS=1, only UDP, ICMP, IGMP and ARP |
| on the PC | collector.py: finds devices, writes CSV, requests resends of gaps |
The four packet kinds differ in the "type" field. A measurement (MEAS) goes to everyone at once: there can be several collectors, and the devices do not need to know their addresses. The collector sends the WHO question to everyone; the device returns the HERE answer only to the one who asked. GET is a request to resend the packet with a given number.
Broadcast is the simplest way to "tell everyone", but it has a limit: a router does not pass it (section 108). The collector and the devices must be in one subnet; to talk through a router you need the collector's address and ordinary unicast sending.
198ж.2 A custom packet: bytes, order, alignment
The format is described in bytes and offsets, as section 126.7 requires. The structure
in the header file is only a description made of byte arrays, and the
_Static_assert checks keep it from drifting away from the offsets:
_Static_assert(sizeof(struct us_hdr_wire) == US_HDR_LEN, "заголовок — 16 байт");
_Static_assert(offsetof(struct us_hdr_wire, seq) == US_OFF_SEQ, "номер — с байта 4");
_Static_assert(offsetof(struct us_hdr_wire, mac) == US_OFF_MAC, "MAC — с байта 8");
_Static_assert(offsetof(struct us_hdr_wire, len) == US_OFF_LEN, "длина — байт 15");
_Static_assert(_Alignof(struct us_hdr_wire) == 1, "массивы байтов: выравнивание 1");
_Static_assert(US_HDR_LEN + US_MEAS_LEN + US_CRC_LEN == 26, "пакет измерения — 26 байт");
_Static_assert(US_HDR_LEN + US_HERE_LEN + US_CRC_LEN == 32, "ответ на WHO — 32 байта");
The packet is built byte by byte, most significant byte first; CRC-16/XMODEM (section 194) is computed over the header and the data:
put32(out + US_OFF_SEQ, seq);
memcpy(out + US_OFF_MAC, mac, 6);
out[US_OFF_FLAGS] = flags;
out[US_OFF_LEN] = len;
if (len)
memcpy(out + US_HDR_LEN, data, len);
put16(out + US_HDR_LEN + len, us_crc16(out, US_HDR_LEN + (size_t)len));
Measurement number 300 from device 06:42:8F:70:AA:C7 (the address from section 105.3), uptime 300000 ms, 23.45 °C, 3300 mV — that is 26 bytes:
55 53 01 01 00 00 01 2c 06 42 8f 70 aa c7 00 08 00 04 93 e0 09 29 0c e4 a3 64
Number 300 is stored as 00 00 01 2c. A "natural" structure with
uint32_t fields would give the same 24 bytes before the CRC — the fields are deliberately placed on their
boundaries, there is no padding — but in Cortex-M and x86 memory the number is stored
as 2c 01 00 00. Sending such a structure as a whole means leaving the
byte order to the processor. The CRC check value for the string
"123456789" is 0x31C3; one flipped bit in the temperature, a truncated
datagram and a foreign magic word give three different parse failures
(−5, −4 and −2 in us_parse). The same format on the PC is us_proto.py using
struct with the format >2sBBI6sBB; the C and Python vectors matched byte for byte.
Why a CRC of our own when UDP has a checksum? In IPv4 the UDP checksum is optional — zero in the field means "no checksum" (section 106.1) — and it is weaker than a CRC. Our own CRC makes the packet self-contained: it is checked the same way whether it came over the network, from a log file or over UART. COBS from section 194, on the other hand, is not needed here: the datagram itself sets the packet boundaries, and the length is known.
Alignment shows up on reception. The Linux device recorded the address of the
data of a received datagram modulo 4 — for all three it came out as 2: the 14 bytes
of the Ethernet header shift everything after them. That is why the fields are read
byte by byte. armclang built us_rd32 like this:
us_rd32: LDR r0,[r0,#0]
REV r0,r0
BX lr
Four byte reads became one LDR and a REV byte swap: the M4 can
read an unaligned word (section 126.3). On the M0 the same source gives
byte-by-byte reads — also correct, whereas a pointer cast to
uint32_t * would fault there.
198ж.3 The device on the lwIP raw API
Without an OS (NO_SYS=1) the main loop drives the stack: receive a frame, run the
lwIP timers, ask the device whether it is time to measure. Sending goes through a pbuf:
static void send_to(const uint8_t *b, size_t n, const ip_addr_t *dst, u16_t port)
{
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM);
if (p == NULL) { /* память стека кончилась — пакет пропадёт, как в сети */
st.no_pbuf++;
return;
}
memcpy(p->payload, b, n); /* PBUF_RAM — один непрерывный кусок */
udp_sendto(pcb, p, dst, port); /* место под заголовки UDP, IP и Ethernet уже оставлено */
pbuf_free(p); /* udp_sendto буфер не освобождает */
}
PBUF_TRANSPORT reserves room in front of the data for the headers of the lower
layers, so lwIP does not copy the packet again. The documentation in udp.c
says it plainly: "The pbuf is not deallocated" — the sender frees it.
On reception it is the other way round: according to udp.h the handler frees the buffer, and the
sender address may point into that buffer. That is why the handler
copies the address first and then calls pbuf_free:
ip_addr_copy(who_addr, *addr); /* addr указывает внутрь p: копировать до pbuf_free */
who_port = port;
who_seq = pk.seq; /* ответ несёт номер вопроса */
who_pending = 1;
Data is read with pbuf_copy_partial — a received packet may sit in a
chain of pbufs, and p->payload is only the first piece.
The device computes the broadcast address itself — from the address and the mask (section 105.5): the host part is filled with ones.
ip_addr_set_ip4_u32(&dst, ip4_addr_get_u32(netif_ip4_addr(nif)) | ~ip4_addr_get_u32(netif_ip4_netmask(nif)));
On the F407 the same files sit on top of the driver from 106а: port_f407.c
includes eth106a.c (RMII, MDIO, receive ring), adds one
transmit descriptor, a sys_now clock from SysTick and reading of the temperature sensor
and VREFINT using the factory calibration points (addresses 0x1FFF7A2A,
0x1FFF7A2C and 0x1FFF7A2E — from stm32f4xx_ll_adc.h). The driver takes the length of a received
frame from the FL field, and according to RM0090 it includes the CRC — the four
FCS bytes are not passed to lwIP.
armclang -Os, -Wall -Wextra — zero warnings. In the built
image (the linker threw out everything unused):
| Part | Code and constants | RAM |
|---|---|---|
| lwIP: UDP, IPv4, ICMP, IGMP, ARP | 9166 bytes | 12048 bytes |
| device, packet, F407 port | 2266 bytes | 8287 bytes |
| whole image | 12572 bytes | 22216 bytes |
The lwIP RAM is almost entirely the pools from lwipopts.h: six receive buffers of
1536 bytes and a 2 KB heap for packets being sent. The device's own RAM is
mostly the Ethernet buffers (four for receive and one for transmit, each
1536 bytes) and the measurement ring. The whole image also includes the stack and the heap
from the startup file.
198ж.4 Discovery: "who is here?"
The collector does not know the device addresses. It sends WHO to the broadcast address — or to the group — and every device answers with HERE: MAC, IP, mask, firmware version, period. If all of them answered instantly, the answers would arrive in one burst and might not fit into the queues of the switch and the collector. That is why the device delays its answer by 0…99 ms — based on the low bytes of its MAC:
sys_timeout((u32_t)(cfg.mac[4] << 8 | cfg.mac[5]) % 100u, here_later, NULL);
In the experiment the delays came out as 19, 8 and 9 ms — exactly 0xAAC7 % 100,
0xAABC % 100 and 0xAABD % 100 for the three MACs. The answer carries the number of the question:
the collector knows which of its WHO packets a HERE belongs to.
The delayed answer is a timer of our own, sys_timeout, and the lwIP timer pool
is sized by the enabled modules: LWIP_NUM_SYS_TIMEOUT_INTERNAL in
opt.h — with TCP, DHCP and fragment reassembly disabled in lwipopts.h that is ARP and IGMP, two. Our own timer adds one:
#define MEMP_NUM_SYS_TIMEOUT (LWIP_NUM_SYS_TIMEOUT_INTERNAL + 1)
Without this line the device printed "sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty" on the very first WHO and never answered again (198ж.7).
A group instead of broadcast. The device joins 239.19.8.1
(igmp_joingroup_netif); a frame to the group goes to MAC 01:00:5E:13:08:01 —
the low 23 bits of the group address. The benefit is that hosts which have not joined the group
drop such frames already in the MAC, while a broadcast frame
must be received and parsed by every host. The F407 has a MAC filter for this;
in port_f407.c it is set up in the simplest way — "pass all multicast"
(PAM), and lwIP does the selection. In the experiment the third device sent its measurements to the
group, and the collector received them just like the broadcast ones.
198ж.5 Losses: number, resend, acknowledgement
UDP promises neither delivery nor order (section 106.1). What to do about it is up to the protocol, and there are three options:
| Method | What it gives | Cost |
|---|---|---|
| a number in every packet | the collector knows how much and what was lost | 4 bytes |
| resend on request | a gap is recovered if it is still in the ring | a ring in RAM, a GET packet |
| acknowledge every packet | the sender knows about every delivery | an exchange per packet — almost TCP |
For telemetry the first two make sense: the number always, the resend for those gaps that matter. Acknowledging every packet does not go well with broadcast: which of the collectors should acknowledge? If you need guaranteed delivery, that is already TCP or MQTT (section 198и).
The collector keeps the expected number for each MAC. A larger one arrives — that is a gap:
if d["next"] is not None and pk["seq"] > d["next"]:
d["gaps"] += 1 # дырка в номерах: чего-то не хватает
for q in range(d["next"], pk["seq"]):
d["miss"].add(q)
d["asked"] += 1
s.sendto(up.build(up.GET, 0, data=struct.pack(">I", q)), src) # повтор — только нам
The device answers from a ring of the last 32 measurements: with a 1 s period that is
32 seconds of history. A resend carries the US_FLAG_REPEAT flag and the original
measurement time, so the gap takes its proper place in the log:
6052,56:2f:8f:70:aa:bc,7,1486696,21.84,3300,0
6052,56:2f:8f:70:aa:bc,6,1485696,21.83,3300,1
Packet 7 came instead of 6; within the same millisecond the collector asked for 6 and
received it with a device time 1000 ms earlier. A number already pushed out of the
ring is not resent — the device counts such a request in get_miss.
198ж.6 Experiment: three devices and a collector in a docker network
QEMU does not model the F407 Ethernet, so the device runs under Linux:
main_linux.c opens a raw AF_PACKET socket on the container's eth0,
lwIP has its own MAC and IP, and the container kernel takes no part in the exchange. The network is
10.198.8.0/24, with three devices (.101, .102, .103) and the collector (.10). The device itself
creates the loss — the second one skips every 7th measurement, the third
every 10th, but puts them into the ring: for the collector this is exactly a loss
in the network. There is no tc netem in the image. 5 s after start the kernel of one of the
containers sends a foreign datagram "hello" to port 19800.
A 45 s run (run_host.txt):
| Device | To | Received | Gaps | Resent | Lost |
|---|---|---|---|---|---|
| 06:42:8f:70:aa:c7 | 10.198.8.255 | 45 | 0 | 0 | 0 |
| 56:2f:8f:70:aa:bc | 10.198.8.255 | 39 | 6 | 6 | 0 |
| 0a:31:8f:70:aa:bd | 239.19.8.1 | 41 | 4 | 4 | 0 |
- The collector found 3 devices — both with the broadcast WHO and with the WHO to the group; each one answered twice.
- 125 measurements, 10 gaps, 10 resends, 0 lost. The log has 135 lines: 125 measurements and 10 resends.
- The request for number 0 at the end of the run was not answered by any device: that number
had long been pushed out of the ring, each has
get_miss=1. - The collector rejected "hello" as too short, and so did every device
(
bad=1). - The multicast frames arrived at MAC 01:00:5e:13:08:01.
One finding along the way. The first run was silent: the devices saw neither WHO
nor GET, although the frames did arrive. The Linux kernel hands datagrams to the virtual cable
with an unfinished UDP checksum — the network adapter would complete it,
and for now the field holds only the pseudo-header sum. lwIP checks the checksum
(CHECKSUM_CHECK_UDP) and silently drops such packets. main_linux.c
completes the checksum where the kernel marked it as unfinished — as it would be
on the wire. On the F407 with a real wire this does not happen, but the lesson is general:
a capture on the sending machine does not show what actually went out on the network.
198ж.7 Pitfalls
The device answered WHO once — and never again. The lwIP timer pool
is sized only for its own modules; sys_timeout for the delayed
answer found no room, the answer was never scheduled, and the "answer pending" flag stayed
set. Every sys_timeout of your own adds one to MEMP_NUM_SYS_TIMEOUT.
Python collector: sendto to the broadcast address gives "Permission
denied". Without SO_BROADCAST the kernel does not allow sending to the broadcast
address (checked: errno 13). lwIP allows it by default:
IP_SOF_BROADCAST in opt.h is 0; if you enable it, the pcb will need
SOF_BROADCAST.
The receive handler sometimes answers the wrong host. The sender address sits
inside the received pbuf; after pbuf_free its place is taken by the next
packet. Copy it with ip_addr_copy before freeing.
Over time the device stops sending. The sender forgot
pbuf_free after udp_sendto — the function does not free the buffer, the stack
heap melts away, and pbuf_alloc starts returning NULL.
Parsing faults on the M0, though it worked on the M4. The fields were read through a pointer
cast, and the datagram data lies at an offset of 2 from a word boundary.
Only byte-by-byte reads or memcpy (section 126).
A collector behind a router does not see a single device. Broadcast does not go beyond the subnet (section 108). Put the collector in the same subnet, or give the device its address.
The devices do not see packets from the neighbouring container. An unfinished UDP checksum on the virtual cable (198ж.6): lwIP drops the datagram without a single message.
Python 3.11 socket has no IP_PKTINFO. The constant exists in the Linux
kernel (8), but not in the module; collector.py takes it through getattr
with a default value.
198ж.8 Summary
- The packet is a 16-byte header (magic word, version, type, number, MAC,
flags, length), the data and a CRC-16/XMODEM: a measurement is 26 bytes, WHO 18,
HERE 32, GET 22; the order is most significant byte first, and
_Static_assertholds the offsets. - The data of a received datagram lies at an offset of 2 from a word boundary;
armclang turned the byte-by-byte read into
LDR+REV. - lwIP 2.1.2 without an OS, UDP only: 9166 bytes of code and 12048 bytes of RAM; the device image for the F407 is 12572 bytes of Flash and 22216 bytes of RAM, zero warnings.
- Discovery: WHO to everyone, HERE to the one who asked after 0…99 ms based on the MAC — in the experiment
19, 8 and 9 ms; your own timer adds one to
MEMP_NUM_SYS_TIMEOUT. - Losses: a number in every packet, a ring of 32 measurements, resend on GET — in the experiment 10 gaps out of 10 were closed, 125 measurements from 3 devices in 45 s.
- Broadcast does not pass through a router: the collector must live in the devices' subnet.
Full source code
us_proto.h — the sensor packet format.
/* 198ж: свой двоичный пакет датчика поверх UDP.
Формат описан байтами, а не структурой: старший байт вперёд, без выравнивания.
Один и тот же файл собирается для F407 (armclang) и для Linux (gcc). */
#ifndef US_PROTO_H
#define US_PROTO_H
#include <stddef.h>
#include <stdint.h>
#define US_PORT 19800u /* порт прибора и сборщика */
#define US_MAGIC0 0x55u /* 'U' */
#define US_MAGIC1 0x53u /* 'S' — «UDP Sensor» */
#define US_VERSION 1u
enum us_type {
US_MEAS = 1, /* измерение: прибор → все */
US_WHO = 2, /* «кто здесь?»: ПК → все */
US_HERE = 3, /* ответ на WHO: прибор → спросившему */
US_GET = 4 /* «повтори пакет N»: ПК → прибору */
};
#define US_FLAG_REPEAT 0x01u /* пакет — повтор из кольца по запросу GET */
/* смещения полей заголовка */
enum {
US_OFF_MAGIC = 0, /* 2 байта: 'U' 'S' */
US_OFF_VER = 2, /* 1: версия формата */
US_OFF_TYPE = 3, /* 1: enum us_type */
US_OFF_SEQ = 4, /* 4: номер пакета, старшим байтом вперёд */
US_OFF_MAC = 8, /* 6: MAC прибора — ключ в журнале */
US_OFF_FLAGS = 14, /* 1: US_FLAG_* */
US_OFF_LEN = 15, /* 1: длина данных после заголовка */
US_HDR_LEN = 16
};
#define US_CRC_LEN 2u /* CRC-16/XMODEM всего, что до неё */
#define US_MEAS_LEN 8u /* uptime_ms 4, temp_c100 2, vdd_mv 2 */
#define US_HERE_LEN 14u /* ip 4, mask 4, fw 3, запас 1, period_ms 2 */
#define US_GET_LEN 4u /* номер нужного пакета */
#define US_MAX_PACKET (US_HDR_LEN + 32u + US_CRC_LEN)
/* описание заголовка массивами байтов: выравнивание 1, заполнения нет */
struct us_hdr_wire {
uint8_t magic[2];
uint8_t ver;
uint8_t type;
uint8_t seq[4];
uint8_t mac[6];
uint8_t flags;
uint8_t len;
};
_Static_assert(sizeof(struct us_hdr_wire) == US_HDR_LEN, "заголовок — 16 байт");
_Static_assert(offsetof(struct us_hdr_wire, seq) == US_OFF_SEQ, "номер — с байта 4");
_Static_assert(offsetof(struct us_hdr_wire, mac) == US_OFF_MAC, "MAC — с байта 8");
_Static_assert(offsetof(struct us_hdr_wire, len) == US_OFF_LEN, "длина — байт 15");
_Static_assert(_Alignof(struct us_hdr_wire) == 1, "массивы байтов: выравнивание 1");
_Static_assert(US_HDR_LEN + US_MEAS_LEN + US_CRC_LEN == 26, "пакет измерения — 26 байт");
_Static_assert(US_HDR_LEN + US_HERE_LEN + US_CRC_LEN == 32, "ответ на WHO — 32 байта");
struct us_meas {
uint32_t uptime_ms;
int16_t temp_c100; /* температура в сотых градуса */
uint16_t vdd_mv; /* питание, мВ */
};
struct us_here {
uint8_t ip[4], mask[4];
uint8_t fw[3]; /* версия прошивки: старший, младший, правка */
uint16_t period_ms;
};
struct us_pkt { /* разобранный пакет */
uint8_t type, flags, len;
uint32_t seq;
uint8_t mac[6];
const uint8_t *data; /* данные внутри принятого буфера */
};
uint16_t us_crc16(const uint8_t *p, size_t n);
size_t us_build(uint8_t *out, uint8_t type, uint32_t seq, const uint8_t mac[6], uint8_t flags,
const uint8_t *data, uint8_t len);
size_t us_build_meas(uint8_t *out, uint32_t seq, const uint8_t mac[6], uint8_t flags, const struct us_meas *m);
size_t us_build_here(uint8_t *out, uint32_t seq, const uint8_t mac[6], const struct us_here *h);
int us_parse(const uint8_t *p, size_t n, struct us_pkt *pk);
void us_get_meas(const struct us_pkt *pk, struct us_meas *m);
uint32_t us_rd32(const uint8_t *p);
#endif
us_proto.c — building and parsing the packet byte by byte.
/* 198ж: сборка и разбор пакета датчика — побайтно, старшим байтом вперёд. */
#include <string.h>
#include "us_proto.h"
uint16_t us_crc16(const uint8_t *p, size_t n) /* CRC-16/XMODEM, как в разделе 194 */
{
uint16_t c = 0;
while (n--) {
c ^= (uint16_t)(*p++ << 8);
for (int i = 0; i < 8; i++)
c = (uint16_t)((c & 0x8000u) ? ((unsigned)c << 1) ^ 0x1021u : (unsigned)c << 1);
}
return c;
}
static void put16(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)v;
}
static void put32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
uint32_t us_rd32(const uint8_t *p)
{
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
static uint16_t rd16(const uint8_t *p)
{
return (uint16_t)(p[0] << 8 | p[1]);
}
size_t us_build(uint8_t *out, uint8_t type, uint32_t seq, const uint8_t mac[6], uint8_t flags,
const uint8_t *data, uint8_t len)
{
out[US_OFF_MAGIC] = US_MAGIC0;
out[US_OFF_MAGIC + 1] = US_MAGIC1;
out[US_OFF_VER] = US_VERSION;
out[US_OFF_TYPE] = type;
put32(out + US_OFF_SEQ, seq);
memcpy(out + US_OFF_MAC, mac, 6);
out[US_OFF_FLAGS] = flags;
out[US_OFF_LEN] = len;
if (len)
memcpy(out + US_HDR_LEN, data, len);
put16(out + US_HDR_LEN + len, us_crc16(out, US_HDR_LEN + (size_t)len));
return US_HDR_LEN + (size_t)len + US_CRC_LEN;
}
size_t us_build_meas(uint8_t *out, uint32_t seq, const uint8_t mac[6], uint8_t flags, const struct us_meas *m)
{
uint8_t d[US_MEAS_LEN];
put32(d, m->uptime_ms);
put16(d + 4, (uint16_t)m->temp_c100); /* знак — в дополнительном коде */
put16(d + 6, m->vdd_mv);
return us_build(out, US_MEAS, seq, mac, flags, d, sizeof d);
}
size_t us_build_here(uint8_t *out, uint32_t seq, const uint8_t mac[6], const struct us_here *h)
{
uint8_t d[US_HERE_LEN];
memcpy(d, h->ip, 4);
memcpy(d + 4, h->mask, 4);
memcpy(d + 8, h->fw, 3);
d[11] = 0;
put16(d + 12, h->period_ms);
return us_build(out, US_HERE, seq, mac, 0, d, sizeof d);
}
/* 0 — пакет цел; отрицательное — почему выброшен */
int us_parse(const uint8_t *p, size_t n, struct us_pkt *pk)
{
if (n < US_HDR_LEN + US_CRC_LEN)
return -1; /* короче пустого пакета */
if (p[US_OFF_MAGIC] != US_MAGIC0 || p[US_OFF_MAGIC + 1] != US_MAGIC1)
return -2; /* чужой пакет на нашем порту */
if (p[US_OFF_VER] != US_VERSION)
return -3; /* версия, которую мы не знаем */
if (n != US_HDR_LEN + (size_t)p[US_OFF_LEN] + US_CRC_LEN)
return -4; /* длина не сходится с датаграммой */
if (us_crc16(p, n - US_CRC_LEN) != rd16(p + n - US_CRC_LEN))
return -5;
pk->type = p[US_OFF_TYPE];
pk->flags = p[US_OFF_FLAGS];
pk->len = p[US_OFF_LEN];
pk->seq = us_rd32(p + US_OFF_SEQ);
memcpy(pk->mac, p + US_OFF_MAC, 6);
pk->data = p + US_HDR_LEN;
return 0;
}
void us_get_meas(const struct us_pkt *pk, struct us_meas *m)
{
m->uptime_ms = us_rd32(pk->data);
m->temp_c100 = (int16_t)rd16(pk->data + 4);
m->vdd_mv = rd16(pk->data + 6);
}
sensor_app.c — the device on the lwIP raw API.
/* 198ж: прибор на lwIP raw API (NO_SYS=1): измерение раз в период на широковещательный
адрес подсети или в группу, ответ на «кто здесь?», повтор пакета по номеру. */
#include <string.h>
#include "lwip/igmp.h"
#include "lwip/pbuf.h"
#include "lwip/sys.h"
#include "lwip/timeouts.h"
#include "lwip/udp.h"
#include "sensor_app.h"
static struct udp_pcb *pcb;
static struct netif *nif;
static struct sensor_cfg cfg;
static struct sensor_stats st;
static uint32_t seq, next_ms;
static struct { uint32_t seq; struct us_meas m; } ring[US_RING];
static ip_addr_t who_addr; /* кому ответить на WHO — копия, не указатель в pbuf */
static u16_t who_port;
static uint32_t who_seq;
static uint8_t who_pending;
static void send_to(const uint8_t *b, size_t n, const ip_addr_t *dst, u16_t port)
{
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM);
if (p == NULL) { /* память стека кончилась — пакет пропадёт, как в сети */
st.no_pbuf++;
return;
}
memcpy(p->payload, b, n); /* PBUF_RAM — один непрерывный кусок */
udp_sendto(pcb, p, dst, port); /* место под заголовки UDP, IP и Ethernet уже оставлено */
pbuf_free(p); /* udp_sendto буфер не освобождает */
}
static void here_later(void *arg)
{
struct us_here h;
uint8_t b[US_MAX_PACKET];
(void)arg;
memcpy(h.ip, netif_ip4_addr(nif), 4);
memcpy(h.mask, netif_ip4_netmask(nif), 4);
memcpy(h.fw, cfg.fw, 3);
h.period_ms = cfg.period_ms;
send_to(b, us_build_here(b, who_seq, cfg.mac, &h), &who_addr, who_port);
who_pending = 0;
st.here++;
}
static void on_recv(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
uint8_t b[US_MAX_PACKET];
struct us_pkt pk;
(void)arg;
(void)upcb;
st.rx_payload_mod4 = (uint32_t)((uintptr_t)p->payload & 3u);
u16_t n = pbuf_copy_partial(p, b, sizeof b, 0); /* данные могут лежать в цепочке pbuf */
if (p->tot_len > sizeof b || us_parse(b, n, &pk) != 0) {
st.bad++;
} else if (pk.type == US_WHO && !who_pending) {
ip_addr_copy(who_addr, *addr); /* addr указывает внутрь p: копировать до pbuf_free */
who_port = port;
who_seq = pk.seq; /* ответ несёт номер вопроса */
who_pending = 1;
/* ответы разнесены по времени: 0…99 мс по младшим байтам MAC */
sys_timeout((u32_t)(cfg.mac[4] << 8 | cfg.mac[5]) % 100u, here_later, NULL);
} else if (pk.type == US_GET && pk.len == US_GET_LEN) {
uint32_t want = us_rd32(pk.data);
if (ring[want % US_RING].seq == want && want < seq) {
uint8_t o[US_MAX_PACKET];
size_t k = us_build_meas(o, want, cfg.mac, US_FLAG_REPEAT, &ring[want % US_RING].m);
send_to(o, k, addr, port); /* повтор — только спросившему */
st.repeats++;
} else {
st.get_miss++; /* из кольца уже вытеснен */
}
}
pbuf_free(p); /* принятый буфер освобождает обработчик */
}
void sensor_start(struct netif *n, const struct sensor_cfg *c)
{
ip4_addr_t grp;
nif = n;
cfg = *c;
for (unsigned i = 0; i < US_RING; i++)
ring[i].seq = 0xFFFFFFFFu; /* пустая ячейка */
ip4_addr_set_u32(&grp, PP_HTONL(US_MCAST));
igmp_joingroup_netif(nif, &grp); /* WHO может прийти и в группу */
pcb = udp_new();
udp_bind(pcb, IP_ANY_TYPE, US_PORT);
udp_recv(pcb, on_recv, NULL);
next_ms = sys_now() + cfg.period_ms;
}
void sensor_poll(uint32_t now_ms)
{
ip_addr_t dst;
struct us_meas m;
uint8_t b[US_MAX_PACKET];
if ((int32_t)(now_ms - next_ms) < 0)
return; /* сравнение по знаку разности: переживает переполнение */
next_ms += cfg.period_ms;
sensor_read(&m);
ring[seq % US_RING].seq = seq;
ring[seq % US_RING].m = m;
if (cfg.meas_to_mcast)
ip_addr_set_ip4_u32(&dst, PP_HTONL(US_MCAST));
else /* широковещательный адрес подсети: узел — все единицы */
ip_addr_set_ip4_u32(&dst, ip4_addr_get_u32(netif_ip4_addr(nif)) | ~ip4_addr_get_u32(netif_ip4_netmask(nif)));
if (cfg.skip_every && seq % cfg.skip_every == cfg.skip_every - 1u) {
st.skipped++; /* опыт: «потерян в сети», но лежит в кольце */
} else {
send_to(b, us_build_meas(b, seq, cfg.mac, 0, &m), &dst, US_PORT);
st.sent++;
}
seq++;
}
const struct sensor_stats *sensor_get_stats(void)
{
return &st;
}
collector.py — the collector on the computer.
# -*- coding: utf-8 -*-
"""198ж: сборщик на ПК — находит приборы в сети, пишет журнал измерений, замечает пропуски
по номерам и просит повтор (GET). Только стандартная библиотека.
python3 collector.py <широковещательный адрес> <группа> <секунд> <журнал.csv>"""
import socket, struct, sys, time
import us_proto as up
BCAST, GROUP, RUN, LOG = sys.argv[1], sys.argv[2], float(sys.argv[3]), sys.argv[4]
IP_PKTINFO = getattr(socket, "IP_PKTINFO", 8) # в Linux — 8; в модуле socket его может не быть
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # без него sendto на .255 — «Permission denied»
s.setsockopt(socket.IPPROTO_IP, IP_PKTINFO, 1) # узнать, на какой адрес пришла датаграмма
s.bind(("", up.PORT))
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(GROUP) + socket.inet_aton("0.0.0.0"))
s.settimeout(0.05)
t0 = time.monotonic()
now_ms = lambda: (time.monotonic() - t0) * 1000.0
found, devs, bad, own = {}, {}, {}, 0
log = open(LOG, "w")
log.write("t_ms,mac,seq,uptime_ms,temp_c,vdd_mv,repeat\n")
plan = [(1000, "who", BCAST, 0x198), (2000, "who", GROUP, 0x199)]
who_sent, old_get = {}, None
def send(pkt, addr):
s.sendto(pkt, (addr, up.PORT))
while now_ms() < RUN * 1000:
while plan and now_ms() >= plan[0][0]:
_, _, addr, q = plan.pop(0)
send(up.build(up.WHO, q), addr)
who_sent[q] = (now_ms(), addr)
if old_get is None and now_ms() >= RUN * 1000 - 1500:
old_get = now_ms() # номер 0 давно вытеснен из кольца
for d in devs.values():
s.sendto(up.build(up.GET, 0, data=struct.pack(">I", 0)), d["addr"])
try:
p, anc, _, src = s.recvmsg(256, 64)
except socket.timeout:
continue
dst = "?"
for lvl, typ, data in anc:
if lvl == socket.IPPROTO_IP and typ == IP_PKTINFO:
dst = socket.inet_ntoa(data[8:12]) # in_pktinfo.ipi_addr — адрес из заголовка IP
pk, why = up.parse(p)
if pk is None:
bad[why] = bad.get(why, 0) + 1
continue
if pk["type"] in (up.WHO, up.GET):
own += 1 # свой же вопрос вернулся петлёй
continue
mac = up.mac_str(pk["mac"])
if pk["type"] == up.HERE:
ip, mask, fw, per = up.here(pk["data"])
t_who, via = who_sent[pk["seq"]]
found.setdefault(mac, []).append("here mac=%s ip=%s mask=%s fw=%d.%d.%d period=%d who=%s delay_ms=%.0f" % (
mac, socket.inet_ntoa(ip), socket.inet_ntoa(mask), fw[0], fw[1], fw[2], per, via, now_ms() - t_who))
continue
up_ms, t100, vdd = up.meas(pk["data"])
rep = pk["flags"] & up.FLAG_REPEAT
d = devs.setdefault(mac, dict(addr=src, next=None, n=0, first=pk["seq"], last=0, gaps=0, miss=set(), asked=0,
rec=0, dup=0, dst=set(), old=0))
log.write("%.0f,%s,%d,%d,%.2f,%d,%d\n" % (now_ms(), mac, pk["seq"], up_ms, t100 / 100.0, vdd, rep))
if rep:
if pk["seq"] in d["miss"]:
d["miss"].discard(pk["seq"])
d["rec"] += 1
elif pk["seq"] == 0 and old_get is not None:
d["old"] += 1
continue
d["n"] += 1
d["dst"].add(dst)
if d["next"] is not None and pk["seq"] > d["next"]:
d["gaps"] += 1 # дырка в номерах: чего-то не хватает
for q in range(d["next"], pk["seq"]):
d["miss"].add(q)
d["asked"] += 1
s.sendto(up.build(up.GET, 0, data=struct.pack(">I", q)), src) # повтор — только нам
elif d["next"] is not None and pk["seq"] < d["next"]:
d["dup"] += 1 # старый номер: повтор или перестановка
continue
d["next"] = pk["seq"] + 1
d["last"] = pk["seq"]
log.close()
print("found %d devices" % len(found))
for mac in sorted(found):
for line in found[mac]:
print(line)
tot = dict(n=0, gaps=0, asked=0, rec=0, lost=0)
for mac in sorted(devs):
d = devs[mac]
print("dev mac=%s from=%s:%d to=%s meas=%d seq=%d..%d gaps=%d asked=%d recovered=%d lost=%d dup=%d old_get_answers=%d" % (
mac, d["addr"][0], d["addr"][1], ",".join(sorted(d["dst"])), d["n"], d["first"], d["last"], d["gaps"],
d["asked"], d["rec"], len(d["miss"]), d["dup"], d["old"]))
for k in ("n", "gaps", "asked", "rec"):
tot[k] += d[k]
tot["lost"] += len(d["miss"])
print("total meas=%(n)d gaps=%(gaps)d asked=%(asked)d recovered=%(rec)d lost=%(lost)d" % tot,
"own_loopback=%d bad=%s" % (own, sorted(bad.items())))
Next: section 198з "Web page and REST on STM32: lwIP httpd, SSI and CGI" — the device is on the network; now you can open it in a browser.