Skip to content

Commit f62dfa6

Browse files
lexus2kCopilot
andcommitted
Release v1.2.0: Python bindings update, README overhaul
Python bindings: - Fix on_read/on_send callbacks to pass peer address as first arg - Add NRM mode support: mode, addr, peers_count properties - Add send_to(), register_peer() for multi-peer NRM - Add send_ui(), send_ui_to() for UI frames - Add on_read_ui callback for receiving UI frames - Add set_ka_timeout() method - Add window_size property - Export module constants (MODE_ABM, MODE_NRM, CRC_*, PRIMARY_ADDR) - Remove leftover test function from module - Fix module name from 'tinyproto_' to 'tinyproto' Documentation: - Complete README rewrite with protocol mode diagrams - Add architecture overview, frame type table - Add C API, C++ API, and Python quick start examples - Add API reference tables - Add testing section with test group names - Fix Python example (tx() takes no args, use put() first) Version bump: 1.1.1 -> 1.2.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 007e4ca commit f62dfa6

7 files changed

Lines changed: 634 additions & 205 deletions

File tree

README.md

Lines changed: 370 additions & 172 deletions
Large diffs are not rendered by default.

Release_notes.txt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,43 @@
1+
== ver. 1.2.0 =======================================
2+
- HDLC ABM compliance improvements:
3+
- Added DM (Disconnected Mode) frame response
4+
- Added FRMR (Frame Reject) handler
5+
- Added RSET (Reset) handler
6+
- Added RNR (Receiver Not Ready) recognition
7+
- Added P/F bit enforcement in ABM mode
8+
- Added UI (Unnumbered Information) frames with send/receive API
9+
- Added SREJ (Selective Reject) S-frame support
10+
- NRM (Normal Response Mode) completion:
11+
- Fixed P/F burst (only last I-frame in burst gets P/F bit)
12+
- Fixed CR bit on idle-path SNRM
13+
- Added disconnected state frame filtering
14+
- Comprehensive NRM test coverage (15 tests)
15+
- C++ wrapper reliability fixes:
16+
- Fixed Light::write(IPacket) return value (was bool, now byte count)
17+
- Fixed IFd::run_tx infinite loop on zero-write
18+
- Increased IFd::run_rx buffer from 4 to 64 bytes
19+
- Fixed IHdlcLinkLayer flush flag race condition
20+
- Fixed ISerialLinkLayer::runTx infinite loop
21+
- Fixed FdD destructor type mismatch (uintptr_t[] vs uint8_t[])
22+
- Fixed Proto::end() null pointer check
23+
- Fixed Proto::getLostRxFrames() mutex protection
24+
- Python bindings updated:
25+
- Added NRM mode support (mode, addr, peers_count properties)
26+
- Added send_to(), register_peer() for multi-peer NRM
27+
- Added send_ui(), send_ui_to() for UI frames
28+
- Added on_read_ui callback for receiving UI frames
29+
- Added set_ka_timeout() method
30+
- Added window_size property
31+
- Fixed on_read/on_send callbacks to pass peer address
32+
- Exported module constants (MODE_ABM, MODE_NRM, CRC_*, PRIMARY_ADDR)
33+
- Removed leftover test function from module
34+
- CI/CD: pinned setuptools<78 for ESP-IDF v4.3 compatibility
35+
- Added .gitignore for build artifacts
36+
- Comprehensive unit tests: 101 tests, 1873 checks
37+
- Updated documentation with protocol mode diagrams and examples
38+
39+
== ver. 1.1.1 =======================================
40+
141
== ver. 0.9.3 =======================================
242
- Fixed protocol for Arduino Zero/M0+
343

library.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Tiny Protocol",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"description": "Tiny Protocol is layer 2 simple protocol. It is intended to be used for the systems with small amount of resources",
55
"keywords": "protocol communication protocol-library hdlc-like hdlc arduino microcontroller",
66
"authors":

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=tinyproto
2-
version=1.1.1
2+
version=1.2.0
33
author=Alexey Dynda
44
maintainer=Alexey Dynda <alexey.dynda@gmail.com>
55
sentence=Allows to communicate other boards/PC via physical connection.

python/fd.cpp

Lines changed: 201 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,31 @@ typedef struct
4141
hdlc_crc_t crc_type;
4242
int mtu;
4343
int window_size;
44+
int mode;
45+
int peers_count;
46+
int addr;
4447
void *buffer;
4548
PyObject *on_frame_send;
4649
PyObject *on_frame_read;
50+
PyObject *on_frame_read_ui;
4751
PyObject *on_connect_event;
4852
PyObject *read_func;
4953
PyObject *write_func;
5054
int error_flag;
5155
} Fd;
5256

5357
static PyMemberDef Fd_members[] = {
54-
{"mtu", T_INT, offsetof(Fd, mtu), 0, "Maximum size of payload"}, {NULL} /* Sentinel */
58+
{"mtu", T_INT, offsetof(Fd, mtu), 0, "Maximum size of payload"},
59+
{"window_size", T_INT, offsetof(Fd, window_size), 0, "Sliding window size (1-7)"},
60+
{NULL} /* Sentinel */
5561
};
5662

5763
/////////////////////////////// ALLOC/DEALLOC
5864

5965
static void Fd_dealloc(Fd *self)
6066
{
6167
Py_XDECREF(self->on_frame_read);
68+
Py_XDECREF(self->on_frame_read_ui);
6269
Py_XDECREF(self->on_frame_send);
6370
Py_XDECREF(self->on_connect_event);
6471
Py_XDECREF(self->read_func);
@@ -73,11 +80,15 @@ static int Fd_init(Fd *self, PyObject *args, PyObject *kwds)
7380
self->crc_type = HDLC_CRC_16;
7481
self->on_frame_send = NULL;
7582
self->on_frame_read = NULL;
83+
self->on_frame_read_ui = NULL;
7684
self->on_connect_event = NULL;
7785
self->read_func = NULL;
7886
self->write_func = NULL;
7987
self->mtu = 1500;
8088
self->window_size = 7;
89+
self->mode = TINY_FD_MODE_ABM;
90+
self->peers_count = 0;
91+
self->addr = 0;
8192
self->error_flag = 0;
8293
return 0;
8394
}
@@ -104,10 +115,31 @@ static void on_frame_read(void *user_data, uint8_t addr, uint8_t *data, int len)
104115
PyGILState_STATE gstate;
105116
gstate = PyGILState_Ensure();
106117

118+
PyObject *arg_addr = PyLong_FromLong((long)addr);
107119
PyObject *arg = PyByteArray_FromStringAndSize((const char *)data, (Py_ssize_t)len);
108-
PyObject *temp = PyObject_CallFunctionObjArgs(self->on_frame_read, arg, NULL);
109-
Py_XDECREF(temp); // Dereference result
110-
Py_DECREF(arg); // We do not need ByteArray anymore
120+
PyObject *temp = PyObject_CallFunctionObjArgs(self->on_frame_read, arg_addr, arg, NULL);
121+
Py_XDECREF(temp);
122+
Py_DECREF(arg);
123+
Py_DECREF(arg_addr);
124+
125+
PyGILState_Release(gstate);
126+
}
127+
}
128+
129+
static void on_frame_read_ui(void *user_data, uint8_t addr, uint8_t *data, int len)
130+
{
131+
Fd *self = (Fd *)user_data;
132+
if ( self->on_frame_read_ui )
133+
{
134+
PyGILState_STATE gstate;
135+
gstate = PyGILState_Ensure();
136+
137+
PyObject *arg_addr = PyLong_FromLong((long)addr);
138+
PyObject *arg = PyByteArray_FromStringAndSize((const char *)data, (Py_ssize_t)len);
139+
PyObject *temp = PyObject_CallFunctionObjArgs(self->on_frame_read_ui, arg_addr, arg, NULL);
140+
Py_XDECREF(temp);
141+
Py_DECREF(arg);
142+
Py_DECREF(arg_addr);
111143

112144
PyGILState_Release(gstate);
113145
}
@@ -121,10 +153,12 @@ static void on_frame_send(void *user_data, uint8_t addr, const uint8_t *data, in
121153
PyGILState_STATE gstate;
122154
gstate = PyGILState_Ensure();
123155

156+
PyObject *arg_addr = PyLong_FromLong((long)addr);
124157
PyObject *arg = PyByteArray_FromStringAndSize((const char *)data, (Py_ssize_t)len);
125-
PyObject *temp = PyObject_CallFunctionObjArgs(self->on_frame_send, arg, NULL);
126-
Py_XDECREF(temp); // Dereference result
127-
Py_DECREF(arg); // We do not need ByteArray anymore
158+
PyObject *temp = PyObject_CallFunctionObjArgs(self->on_frame_send, arg_addr, arg, NULL);
159+
Py_XDECREF(temp);
160+
Py_DECREF(arg);
161+
Py_DECREF(arg_addr);
128162

129163
PyGILState_Release(gstate);
130164
}
@@ -160,15 +194,21 @@ static PyObject *Fd_begin(Fd *self)
160194
init.on_read_cb = on_frame_read;
161195
init.on_send_cb = on_frame_send;
162196
init.on_connect_event_cb = on_connect_event;
197+
init.on_read_ui_cb = on_frame_read_ui;
163198
init.crc_type = self->crc_type;
164-
init.buffer_size = tiny_fd_buffer_size_by_mtu_ex(1, self->mtu, self->window_size, init.crc_type, 2);
199+
init.buffer_size = tiny_fd_buffer_size_by_mtu_ex(
200+
self->peers_count > 0 ? self->peers_count : 1,
201+
self->mtu, self->window_size, init.crc_type, 2);
165202
self->buffer = PyObject_Malloc(init.buffer_size);
166203
init.buffer = self->buffer;
167204
init.send_timeout = 1000;
168205
init.retry_timeout = 200;
169206
init.retries = 2;
170207
init.window_frames = self->window_size;
171208
init.mtu = self->mtu;
209+
init.mode = (uint8_t)self->mode;
210+
init.peers_count = (uint8_t)self->peers_count;
211+
init.addr = (uint8_t)self->addr;
172212
int result = tiny_fd_init(&self->handle, &init);
173213
return PyLong_FromLong((long)result);
174214
}
@@ -372,6 +412,69 @@ static PyObject *Fd_get_status(Fd *self)
372412
return PyLong_FromLong((long)result);
373413
}
374414

415+
static PyObject *Fd_send_to(Fd *self, PyObject *args)
416+
{
417+
int addr;
418+
Py_buffer buffer{};
419+
if ( !PyArg_ParseTuple(args, "is*", &addr, &buffer) )
420+
{
421+
return NULL;
422+
}
423+
int result;
424+
Py_BEGIN_ALLOW_THREADS;
425+
result = tiny_fd_send_packet_to(self->handle, (uint8_t)addr, buffer.buf, buffer.len, 1000);
426+
Py_END_ALLOW_THREADS;
427+
PyBuffer_Release(&buffer);
428+
return PyLong_FromLong((long)result);
429+
}
430+
431+
static PyObject *Fd_register_peer(Fd *self, PyObject *args)
432+
{
433+
int addr;
434+
if ( !PyArg_ParseTuple(args, "i", &addr) )
435+
{
436+
return NULL;
437+
}
438+
int result = tiny_fd_register_peer(self->handle, (uint8_t)addr);
439+
return PyLong_FromLong((long)result);
440+
}
441+
442+
static PyObject *Fd_send_ui(Fd *self, PyObject *args)
443+
{
444+
Py_buffer buffer{};
445+
if ( !PyArg_ParseTuple(args, "s*", &buffer) )
446+
{
447+
return NULL;
448+
}
449+
int result = tiny_fd_send_ui_packet(self->handle, buffer.buf, buffer.len);
450+
PyBuffer_Release(&buffer);
451+
return PyLong_FromLong((long)result);
452+
}
453+
454+
static PyObject *Fd_send_ui_to(Fd *self, PyObject *args)
455+
{
456+
int addr;
457+
Py_buffer buffer{};
458+
if ( !PyArg_ParseTuple(args, "is*", &addr, &buffer) )
459+
{
460+
return NULL;
461+
}
462+
int result = tiny_fd_send_ui_packet_to(self->handle, (uint8_t)addr, buffer.buf, buffer.len);
463+
PyBuffer_Release(&buffer);
464+
return PyLong_FromLong((long)result);
465+
}
466+
467+
static PyObject *Fd_set_ka_timeout(Fd *self, PyObject *args)
468+
{
469+
unsigned int timeout;
470+
if ( !PyArg_ParseTuple(args, "I", &timeout) )
471+
{
472+
return NULL;
473+
}
474+
tiny_fd_set_ka_timeout(self->handle, (uint32_t)timeout);
475+
Py_RETURN_NONE;
476+
}
477+
375478
/*
376479
void tiny_fd_set_ka_timeout ( tiny_fd_handle_t handle,
377480
uint32_t keep_alive
@@ -426,7 +529,22 @@ static int Fd_set_on_connect_event(Fd *self, PyObject *value, void *closure)
426529
return 0;
427530
}
428531

429-
static PyObject *Fd_get_crc(Fd *self, void *closure)
532+
static PyObject *Fd_get_on_read_ui(Fd *self, void *closure)
533+
{
534+
Py_INCREF(self->on_frame_read_ui);
535+
return self->on_frame_read_ui;
536+
}
537+
538+
static int Fd_set_on_read_ui(Fd *self, PyObject *value, void *closure)
539+
{
540+
PyObject *tmp = self->on_frame_read_ui;
541+
Py_INCREF(value);
542+
self->on_frame_read_ui = value;
543+
Py_XDECREF(tmp);
544+
return 0;
545+
}
546+
547+
static PyObject* Fd_get_crc(Fd *self, void * closure)
430548
{
431549
return PyLong_FromLong( self->crc_type );
432550
}
@@ -449,15 +567,79 @@ static int Fd_set_crc(Fd *self, PyObject *value)
449567
{
450568
PyErr_Format(PyExc_RuntimeError, "Allowable CRC values are: 0 (AUTO), 8, 16, 32, 255 (OFF)");
451569
}
452-
return result/* 0 on success, -1 on failure with error set. */;
570+
return result;
571+
}
572+
573+
static PyObject *Fd_get_mode(Fd *self, void *closure)
574+
{
575+
return PyLong_FromLong(self->mode);
576+
}
577+
578+
static int Fd_set_mode(Fd *self, PyObject *value, void *closure)
579+
{
580+
if ( value && PyLong_Check(value) )
581+
{
582+
int temp = PyLong_AsLong(value);
583+
if ( temp == TINY_FD_MODE_ABM || temp == TINY_FD_MODE_NRM )
584+
{
585+
self->mode = temp;
586+
return 0;
587+
}
588+
}
589+
PyErr_Format(PyExc_RuntimeError, "Allowable mode values are: 0 (ABM), 1 (NRM)");
590+
return -1;
591+
}
592+
593+
static PyObject *Fd_get_peers_count(Fd *self, void *closure)
594+
{
595+
return PyLong_FromLong(self->peers_count);
596+
}
597+
598+
static int Fd_set_peers_count(Fd *self, PyObject *value, void *closure)
599+
{
600+
if ( value && PyLong_Check(value) )
601+
{
602+
int temp = PyLong_AsLong(value);
603+
if ( temp >= 0 && temp <= 63 )
604+
{
605+
self->peers_count = temp;
606+
return 0;
607+
}
608+
}
609+
PyErr_Format(PyExc_RuntimeError, "peers_count must be 0-63");
610+
return -1;
611+
}
612+
613+
static PyObject *Fd_get_addr(Fd *self, void *closure)
614+
{
615+
return PyLong_FromLong(self->addr);
616+
}
617+
618+
static int Fd_set_addr(Fd *self, PyObject *value, void *closure)
619+
{
620+
if ( value && PyLong_Check(value) )
621+
{
622+
int temp = PyLong_AsLong(value);
623+
if ( temp >= 0 && temp <= 62 )
624+
{
625+
self->addr = temp;
626+
return 0;
627+
}
628+
}
629+
PyErr_Format(PyExc_RuntimeError, "addr must be 0 (primary) or 1-62 (secondary)");
630+
return -1;
453631
}
454632

455633
static PyGetSetDef Fd_getsetters[] = {
456-
{"on_read", (getter)Fd_get_on_read, (setter)Fd_set_on_read, "Callback for incoming messages", NULL},
457-
{"on_send", (getter)Fd_get_on_send, (setter)Fd_set_on_send, "Callback for successfully sent messages", NULL},
634+
{"on_read", (getter)Fd_get_on_read, (setter)Fd_set_on_read, "Callback for incoming I-frames: func(addr, data)", NULL},
635+
{"on_read_ui", (getter)Fd_get_on_read_ui, (setter)Fd_set_on_read_ui, "Callback for incoming UI frames: func(addr, data)", NULL},
636+
{"on_send", (getter)Fd_get_on_send, (setter)Fd_set_on_send, "Callback for successfully sent messages: func(addr, data)", NULL},
458637
{"on_connect_event", (getter)Fd_get_on_connect_event, (setter)Fd_set_on_connect_event,
459-
"Callback for connection status change events", NULL},
460-
{"crc", (getter)Fd_get_crc, (setter)Fd_set_crc, "CRC value", NULL},
638+
"Callback for connection status change: func(addr, connected)", NULL},
639+
{"crc", (getter)Fd_get_crc, (setter)Fd_set_crc, "CRC type: 0 (AUTO), 8, 16, 32, 255 (OFF)", NULL},
640+
{"mode", (getter)Fd_get_mode, (setter)Fd_set_mode, "Protocol mode: 0 (ABM), 1 (NRM)", NULL},
641+
{"peers_count", (getter)Fd_get_peers_count, (setter)Fd_set_peers_count, "Max peers (0-63, NRM primary only)", NULL},
642+
{"addr", (getter)Fd_get_addr, (setter)Fd_set_addr, "Station address: 0 (primary) or 1-62 (secondary)", NULL},
461643
{NULL} /* Sentinel */
462644
};
463645

@@ -467,12 +649,17 @@ static PyMethodDef Fd_methods[] = {
467649
{"begin", (PyCFunction)Fd_begin, METH_NOARGS, "Initializes Fd protocol"},
468650
{"end", (PyCFunction)Fd_end, METH_NOARGS, "Stops Fd protocol"},
469651
{"send", (PyCFunction)Fd_send, METH_VARARGS, "Sends new message to remote side"},
652+
{"send_to", (PyCFunction)Fd_send_to, METH_VARARGS, "Sends message to specific peer: send_to(addr, data)"},
653+
{"send_ui", (PyCFunction)Fd_send_ui, METH_VARARGS, "Sends UI (connectionless) frame"},
654+
{"send_ui_to", (PyCFunction)Fd_send_ui_to, METH_VARARGS, "Sends UI frame to specific peer: send_ui_to(addr, data)"},
655+
{"register_peer", (PyCFunction)Fd_register_peer, METH_VARARGS, "Registers peer address (NRM primary)"},
470656
{"disconnect", (PyCFunction)Fd_disconnect, METH_NOARGS, "Sends disconnect frame"},
471657
{"rx", (PyCFunction)Fd_rx, METH_VARARGS, "Passes rx data"},
472658
{"tx", (PyCFunction)Fd_tx, METH_VARARGS, "Fills specified buffer with tx data"},
473659
{"run_rx", (PyCFunction)Fd_run_rx, METH_VARARGS, "Reads data from user callback and parses them"},
474660
{"run_tx", (PyCFunction)Fd_run_tx, METH_VARARGS, "Writes data to user callback"},
475661
{"get_status", (PyCFunction)Fd_get_status, METH_NOARGS, "Get connection status"},
662+
{"set_ka_timeout", (PyCFunction)Fd_set_ka_timeout, METH_VARARGS, "Set keep-alive timeout in ms"},
476663
{NULL} /* Sentinel */
477664
};
478665

0 commit comments

Comments
 (0)