Skip to content

Commit

Permalink
emulator: added support for multiple requests on an established TCP/I…
Browse files Browse the repository at this point in the history
…P connection
  • Loading branch information
twystd committed Dec 27, 2024
1 parent cc6dbe0 commit 37c5335
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 38 deletions.
2 changes: 1 addition & 1 deletion Rev.0/cli/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build: format
python3 -m compileall .

debug:
python3 main.py get-status
python3 main.py --destination $(ADDRESS) --protocol tcp::pool debug

get-controller:
python3 main.py get-controller
Expand Down
22 changes: 20 additions & 2 deletions Rev.0/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from uhppoted import decode

import tls
import pool

CONTROLLER = 405419896
DOOR = 3
Expand Down Expand Up @@ -97,11 +98,22 @@ def get_controller(u, dest, timeout, args, protocol='udp'):
request = encode.get_controller_request(CONTROLLER)
bind = '0.0.0.0'

reply = _send(request, bind, dest, timeout, True)
reply = _tls(request, bind, dest, timeout, True)
if reply != None:
return decode.get_controller_response(reply)
else:
return None

elif protocol == 'tcp::pool':
request = encode.get_controller_request(CONTROLLER)
bind = '0.0.0.0'

reply = _pool(request, bind, dest, timeout, True)
if reply != None:
return decode.get_controller_response(reply)
else:
return None

else:
controller = (CONTROLLER, dest, protocol)

Expand Down Expand Up @@ -405,7 +417,13 @@ def onEvent(event):


# INTERNAL: TLS handler
def _send(request, bind, dest, timeout, debug):
def _tls(request, bind, dest, timeout, debug):
transport = tls.TLS(bind, debug)

return transport.send(request, dest, timeout)

# INTERNAL: pooled TCP handler
def _pool(request, bind, dest, timeout, debug):
transport = pool.Pool(bind, debug)

return transport.send(request, dest, timeout)
19 changes: 17 additions & 2 deletions Rev.0/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import sys
import time
import traceback

from trace import Trace
Expand All @@ -14,7 +15,7 @@ def main():
usage()
return -1

parser = argparse.ArgumentParser(description='uhppoted-codegen example')
parser = argparse.ArgumentParser(description='uhppoted-breakout CLI')

parser.add_argument('command', type=str, help='command')

Expand Down Expand Up @@ -47,7 +48,7 @@ def main():
default=2.5,
help='(optional) operation timeout (in seconds). Defaults to 2.5.')

parser.add_argument('--protocol', choices=['udp', 'tcp', 'tls'], default='udp', help='transport protocol')
parser.add_argument('--protocol', choices=['udp', 'tcp', 'tcp::pool', 'tls'], default='udp', help='transport protocol')

args = parser.parse_args()
cmd = args.command
Expand All @@ -62,6 +63,20 @@ def main():
print()
print(f'*** ERROR {cmd}: {x}')
print()
elif cmd == 'debug':
try:
for i in range(3):
exec(commands()['get-controller'], args)
time.sleep(5)
except Exception as x:
print()
print(f'*** ERROR {cmd}: {x}')
print()
if debug:
print(traceback.format_exc())

sys.exit(1)

elif cmd in commands():
try:
exec(commands()[cmd], args)
Expand Down
141 changes: 141 additions & 0 deletions Rev.0/cli/pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'''
UHPPOTE TCP communications wrapper.
Implements the functionality to send and receive 64 byte TCP packets to/from a UHPPOTE
access controller using a 'pooled' TCP/IP connection.
'''

import socket
import ssl
import struct
import re
import time
import ipaddress

from uhppoted import net


class Pool:
pool = {}

def __init__(self, bind='0.0.0.0', debug=False):
'''
Initialises a TCP/IP communications wrapper with the bind address.
Parameters:
bind (string) The IPv4 address:port to which to bind when sending a request.
debug (bool) Dumps the sent and received packets to the console if enabled.
Returns:
Initialised TCPPool object.
Raises:
Exception If any of the supplied IPv4 values cannot be translated to a valid IPv4
address:port combination.
'''
self._bind = (bind, 0)
self._debug = debug

def send(self, request, dest_addr, timeout=2.5):
'''
Binds to the bind address from the constructor and connects to the access controller after which it sends
the request and waits 'timeout' seconds for the reply (if any).
Parameters:
request (bytearray) 64 byte request packet.
dest_addr (string) Optional IPv4 address:port of the controller. Defaults to port 60000
if dest_addr does not include a port.
timeout (float) Optional operation timeout (in seconds). Defaults to 2.5s.
Returns:
Received response packet (if any) or None (for set-ip request).
Raises:
Error For any socket related errors.
'''
self.dump(request)

addr = net.resolve(f'{dest_addr}')
pool = self.pool
key = f'{addr[0]}:{addr[1]}'
sock = None

if key in pool:
s = pool[key]
if s.fileno() != -1:
sock = s

if not sock:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if not is_INADDR_ANY(self._bind):
sock.bind(self._bind)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, net.WRITE_TIMEOUT)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, net.READ_TIMEOUT)

sock.connect(addr)

pool[key] = sock

sock.sendall(request)

if request[1] == 0x96:
return None
else:
return _read(sock, timeout=timeout, debug=self._debug)

def dump(self, packet):
'''
Prints a packet to the console as a formatted hexadecimal string if debug was enabled in the
constructor.
Parameters:
packet (bytearray) 64 byte UDP packet.
Returns:
None.
'''
if self._debug:
net.dump(packet)


def is_INADDR_ANY(addr):
if addr == None:
return True

if f'{addr}' == '':
return True

if addr == (('0.0.0.0', 0)):
return True

return False


# TODO convert to asyncio
def _read(sock, timeout=2.5, debug=False):
'''
Waits 2.5 seconds for a single 64 byte packet to be received on the socket. Prints the packet to the console
if debug is True.
Parameters:
sock (socket) Initialised and open UDP socket.
timeout (float) Optional operation timeout (in seconds). Defaults to 2.5s.
debug (bool) Enables dumping the received packet to the console.
Returns:
Received 64 byte UDP packet (or None).
'''
time_limit = net.timeout_to_seconds(timeout)

sock.settimeout(time_limit)

while True:
reply = sock.recv(1024)
if len(reply) == 64:
if debug:
net.dump(reply)
return reply

return None
4 changes: 2 additions & 2 deletions Rev.0/cli/tls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'''
UHPPOTE TCP communications wrapper.
UHPPOTE TLS communications wrapper.
Implements the functionality to send and receive 64 byte TCP packets to/from a UHPPOTE
access controller.
access controller over TLS.
'''

import socket
Expand Down
82 changes: 52 additions & 30 deletions Rev.0/emulator/go/UT0311/tcp.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package UT0311

import (
"errors"
"io"
"net"
"net/netip"
"os"
"time"

codec "github.com/uhppoted/uhppote-core/encoding/UTO311-L0x"
"github.com/uhppoted/uhppote-core/messages"
Expand All @@ -11,6 +15,8 @@ import (
type TCP struct {
}

const READ_TIMEOUT = 30000 * time.Millisecond

func (tcp TCP) listen(received func(any) (any, error)) error {
bind := netip.MustParseAddrPort("0.0.0.0:60000")

Expand All @@ -28,41 +34,57 @@ func (tcp TCP) listen(received func(any) (any, error)) error {
infof("TCP incoming")

go func() {
buffer := make([]byte, 2048)

if N, err := client.Read(buffer); err != nil {
if err := tcp.read(client, received); err != nil {
warnf("TCP read error (%v)", err)
} else {
debugf("TCP received %v bytes from %v", N, client.RemoteAddr())

if request, err := messages.UnmarshalRequest(buffer[0:N]); err != nil {
warnf("TCP %v", err)
} else {
reply, err := received(request)

if err != nil {
warnf("TCP %v", err)
}

if !isnil(reply) {
if packet, err := codec.Marshal(reply); err != nil {
warnf("TCP %v", err)
} else if packet == nil {
warnf("TCP invalid reply packet (%v)", packet)
} else if N, err := client.Write(packet); err != nil {
warnf("TCP %v", err)
} else {
debugf("TCP sent %v bytes to %v", N, client.RemoteAddr())
}
}
}
}
}()
}
}
}
}

func (tcp TCP) read(socket net.Conn, received func(any) (any, error)) error {
defer socket.Close()

if err := client.Close(); err != nil {
warnf("TCP close error (%v)", err)
for {
buffer := make([]byte, 2048)
deadline := time.Now().Add(READ_TIMEOUT)

socket.SetReadDeadline(deadline)

if N, err := socket.Read(buffer); err != nil && errors.Is(err, io.EOF) {
return nil
} else if err != nil && errors.Is(err, os.ErrDeadlineExceeded) {
warnf("TCP closing connection to %v (idle)", socket.RemoteAddr())
return nil
} else if err != nil {
return err
} else {
debugf("TCP received %v bytes from %v", N, socket.RemoteAddr())

if request, err := messages.UnmarshalRequest(buffer[0:N]); err != nil {
warnf("TCP %v", err)
} else {
reply, err := received(request)

if err != nil {
warnf("TCP %v", err)
}

if !isnil(reply) {
if packet, err := codec.Marshal(reply); err != nil {
warnf("TCP %v", err)
} else if packet == nil {
warnf("TCP invalid reply packet (%v)", packet)
} else if N, err := socket.Write(packet); err != nil {
warnf("TCP %v", err)
} else {
debugf("TCP sent %v bytes to %v", N, socket.RemoteAddr())
}
}()
}
}
}
}

return nil
}
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ On some boards, the XOSC can take longer than usual to stabilize. On such boards
- [ ] API
- [x] UDP
- [x] TCP/IP
- [ ] allow multiple requests
- [x] allow multiple requests
- [ ] TLS
- [x] CLI TLS
- [x] mutual auth
Expand Down

0 comments on commit 37c5335

Please sign in to comment.