diff --git a/README.md b/README.md index ee81b07..f355530 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ security assesment, pentest, ISO certification, etc. This project probably doesn't match your needs nor expectations. Be aware. -Your model train may also catch fire while using this software. +> [!CAUTION] +> Your model train may catch fire while using this software. Check out [my own instance](https://daniele.mynarrowgauge.org). @@ -49,7 +50,7 @@ It has been developed with: ## Requirements -- Python 3.10+ +- Python 3.11+ - A USB port when running Arduino hardware (and adaptors if you have a Mac) ## Web portal installation @@ -99,43 +100,52 @@ connected via serial port, to the network, allowing commands to be sent via a TCP socket. A response generated by the DCC++ EX board is sent to all connected clients, providing synchronization between multiple clients (eg. multiple JMRI instances). -Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board (like when -using an ESP8266 module or a [Mega+WiFi board](https://dcc-ex.com/advanced-setup/supported-microcontrollers/wifi-mega.html)). +Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board, like when +using an ESP8266 module, a [Mega+WiFi board](https://dcc-ex.com/reference/hardware/microcontrollers/wifi-mega.html), or an +[ESP32](https://dcc-ex.com/reference/hardware/microcontrollers/esp32.html) (recommended). -### Customize the settings +### Manual setup -The daemon comes with default settings in `config.ini`. -Settings may need to be customized based on your setup. +You'll need [namp-ncat](https://nmap.org/ncat/) , and `stty` to setup the serial port. + +> [!IMPORTANT] +> Other variants of `nc` or `ncat` may not work as expected. + +Then you can run the following commands: + +```bash +$ stty -F /dev/ttyACM0 -echo 115200 +$ ncat -n -k -l 2560 /dev/ttyACM0 +``` + +> [!IMPORTANT] +> You'll might need to change the serial port (`/dev/ttyACM0`) to match your board. + +> [!NOTE] +> Your user will also need access to the device file, so you might need to add it to the `dialout` group. ### Using containers ```bash -$ cd daemons -$ podman build -t dcc/net-to-serial . -$ podman run --group-add keep-groups --device /dev/ttyACM0 -p 2560:2560 dcc/net-to-serial -``` - -### Manual setup - -```bash -$ cd daemons -$ pip install -r requirements.txt -$ python ./net-to-serial.py +$ cd connector +$ podman build -t dcc/connector . +$ podman run -d --group-add keep-groups --device /dev/ttyACM0:/dev/arduino -p 2560:2560 dcc/connector ``` ### Test with a simulator -A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the `net-to-serial.py` -daemon into a container. To run it: +A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the connector +into a container. To run it: ```bash -$ cd daemons/simulator -$ podman build -t dcc/net-to-serial:sim . -$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim +$ cd connector/simulator +$ podman build -t dcc/connector:sim . +$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim ``` -To be continued ... +> [!WARNING] +> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware. ## Screenshots @@ -146,15 +156,12 @@ To be continued ... ![Screenshot 2023-09-18 at 21-59-30 RGS 1930s short train - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/77f9b7c9-27b3-4a65-bad0-26e9cf77e623) - #### Dark mode ![Screenshot 2023-09-18 at 21-58-22 Company RGS - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/c95697c9-0897-46f4-941c-6092271e4743) --- - - ### Backoffice ![image](https://user-images.githubusercontent.com/1818657/175789937-3e4970a2-b37d-44c3-8605-62dabe209c65.png) @@ -166,8 +173,3 @@ To be continued ... ### Rest API ![image](https://user-images.githubusercontent.com/1818657/180622471-ade06c84-c73b-41d5-a2a7-02a95b2ffc02.png) - - - - - diff --git a/connector/Dockerfile b/connector/Dockerfile new file mode 100644 index 0000000..e71266f --- /dev/null +++ b/connector/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:edge + +RUN apk add --no-cache coreutils nmap-ncat + +EXPOSE 2560/tcp + +SHELL ["/bin/ash", "-c"] +CMD stty -F /dev/arduino -echo 115200 && \ + ncat -n -k -l 2560 /dev/arduino diff --git a/connector/README.md b/connector/README.md new file mode 100644 index 0000000..ce8c093 --- /dev/null +++ b/connector/README.md @@ -0,0 +1,19 @@ +# Use a container to implement a serial to net bridge + +This uses `ncat` by [namp](https://nmap.org/ncat/) to bridge a serial port to a network port. The serial port is passed to the Podman command (eg. `/dev/ttyUSB0`) and the network port is `2560`. + +> [!IMPORTANT] +> Other variants of `nc` or `ncat` may not work as expected. + +## Build and run the container + +```bash +$ podman buil -t dcc/bridge . +$ podman run -d --device=/dev/ttyUSB0:/dev/arduino -p 2560:2560 --name dcc-bridge dcc/bridge +``` + +It can be tested with `telnet`: + +```bash +$ telnet localhost 2560 +``` diff --git a/connector/simulator/CommandStation-EX-uno-13488e1.elf b/connector/simulator/CommandStation-EX-uno-13488e1.elf new file mode 100755 index 0000000..b10ddd5 Binary files /dev/null and b/connector/simulator/CommandStation-EX-uno-13488e1.elf differ diff --git a/connector/simulator/Dockerfile b/connector/simulator/Dockerfile new file mode 100644 index 0000000..4e91998 --- /dev/null +++ b/connector/simulator/Dockerfile @@ -0,0 +1,8 @@ +FROM dcc/bridge + +RUN apk update && apk add --no-cache qemu-system-avr \ + && mkdir /io +ADD start.sh /usr/local/bin +ADD CommandStation-EX*.elf /io + +ENTRYPOINT ["/usr/local/bin/start.sh"] diff --git a/connector/simulator/README.md b/connector/simulator/README.md new file mode 100644 index 0000000..b7eaf11 --- /dev/null +++ b/connector/simulator/README.md @@ -0,0 +1,13 @@ +# Connector and AVR simulator + +> [!WARNING] +> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware. + +`qemu-system-avr` tries to use all the CPU cycles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines. + +```bash +$ podman build -t dcc/connector:sim . +$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim +``` + +All traffic will be collected on the container's `stderr` for debugging purposes. diff --git a/daemons/simulator/start.sh b/connector/simulator/start.sh similarity index 71% rename from daemons/simulator/start.sh rename to connector/simulator/start.sh index 6471bc3..402e536 100755 --- a/daemons/simulator/start.sh +++ b/connector/simulator/start.sh @@ -7,7 +7,5 @@ if [ -c /dev/pts/0 ]; then PTY=1 fi -sed -i "s/ttyACM0/pts\/${PTY}/" /opt/dcc/config.ini - qemu-system-avr -machine uno -bios /io/CommandStation-EX*.elf -serial pty -daemonize -/opt/dcc/net-to-serial.py +ncat -n -k -l 2560 -o /dev/stderr /dev/pts/${PTY} diff --git a/daemons/Dockerfile b/daemons/Dockerfile deleted file mode 100644 index e7817c7..0000000 --- a/daemons/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.11-alpine - -RUN mkdir /opt/dcc && pip -q install pyserial -ADD net-to-serial.py config.ini /opt/dcc -RUN python3 -q -m compileall /opt/dcc/net-to-serial.py - -EXPOSE 2560/tcp - -CMD ["python3", "/opt/dcc/net-to-serial.py"] diff --git a/daemons/README.md b/daemons/README.md deleted file mode 100644 index e8dbf61..0000000 --- a/daemons/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## DCC++ EX connector - -See [README.md](../README.md) diff --git a/daemons/config.ini b/daemons/config.ini deleted file mode 100644 index bff0dcd..0000000 --- a/daemons/config.ini +++ /dev/null @@ -1,14 +0,0 @@ -[Daemon] -LogLevel = debug -ListeningIP = 0.0.0.0 -ListeningPort = 2560 -MaxClients = 10 - -[Serial] -# UNO -Port = /dev/ttyACM0 -# Mega WiFi -# Port = /dev/ttyUSB0 -Baudrate = 115200 -# Timeout in milliseconds -Timeout = 50 diff --git a/daemons/net-to-serial.py b/daemons/net-to-serial.py deleted file mode 100755 index cb24d91..0000000 --- a/daemons/net-to-serial.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -import re -import logging -import serial -import asyncio -import configparser - -from pathlib import Path - - -class SerialDaemon: - connected_clients = set() - - def __init__(self, config): - self.ser = serial.Serial( - config["Serial"]["Port"], - timeout=int(config["Serial"]["Timeout"]) / 1000, - ) - self.ser.baudrate = config["Serial"]["Baudrate"] - self.max_clients = int(config["Daemon"]["MaxClients"]) - - def __del__(self): - try: - self.ser.close() - except AttributeError: - pass - - def __read_serial(self): - """Serial reader wrapper""" - response = b"" - while True: - line = self.ser.read_until() - if not line.strip(): # empty line - break - if line.decode().startswith("<*"): - logging.debug("Serial debug: {}".format(line)) - else: - response += line - logging.debug("Serial read: {}".format(response)) - - return response - - def __write_serial(self, data): - """Serial writer wrapper""" - self.ser.write(data) - - async def handle_echo(self, reader, writer): - """Process a request from socket and return the response""" - logging.info( - "Clients already connected: {} (max: {})".format( - len(self.connected_clients), - self.max_clients, - ) - ) - - addr = writer.get_extra_info("peername")[0] - if len(self.connected_clients) < self.max_clients: - self.connected_clients.add(writer) - while True: # keep connection to client open - data = await reader.read(100) - if not data: # client has disconnected - break - logging.info("Received {} from {}".format(data, addr)) - self.__write_serial(data) - response = self.__read_serial() - for client in self.connected_clients: - client.write(response) - await client.drain() - logging.info("Sent: {}".format(response)) - self.connected_clients.remove(writer) - else: - logging.warning( - "TooManyClients: client {} disconnected".format(addr) - ) - - writer.close() - await writer.wait_closed() - - async def return_board(self): - """Return the board signature""" - line = "" - # drain the serial until we are ready to go - self.__write_serial(b"") - while "DCC-EX" not in line: - line = self.__read_serial().decode() - board = re.findall(r"", line)[0] - return board - - -async def main(): - config = configparser.ConfigParser() - config.read( - Path(__file__).resolve().parent / "config.ini" - ) # mimick os.path.join - logging.basicConfig(level=config["Daemon"]["LogLevel"].upper()) - - sd = SerialDaemon(config) - server = await asyncio.start_server( - sd.handle_echo, - config["Daemon"]["ListeningIP"], - config["Daemon"]["ListeningPort"], - ) - addr = server.sockets[0].getsockname() - logging.info("Serving on {} port {}".format(addr[0], addr[1])) - logging.info( - "Proxying to {} (Baudrate: {}, Timeout: {})".format( - config["Serial"]["Port"], - config["Serial"]["Baudrate"], - config["Serial"]["Timeout"], - ) - ) - logging.info("Initializing board") - logging.info("Board {} ready".format(await sd.return_board())) - - async with server: - await server.serve_forever() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/daemons/requirements.txt b/daemons/requirements.txt deleted file mode 100644 index d1babd3..0000000 --- a/daemons/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -PySerial diff --git a/daemons/simulator/CommandStation-EX-uno-7311f2c.elf b/daemons/simulator/CommandStation-EX-uno-7311f2c.elf deleted file mode 100755 index 473475c..0000000 Binary files a/daemons/simulator/CommandStation-EX-uno-7311f2c.elf and /dev/null differ diff --git a/daemons/simulator/Dockerfile b/daemons/simulator/Dockerfile deleted file mode 100644 index 4ba6659..0000000 --- a/daemons/simulator/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM dcc/net-to-serial - -RUN apk update && apk add qemu-system-avr && mkdir /io -ADD start.sh /opt/dcc -ADD CommandStation-EX*.elf /io - -ENTRYPOINT ["/opt/dcc/start.sh"] diff --git a/daemons/simulator/README.md b/daemons/simulator/README.md deleted file mode 100644 index d593ec0..0000000 --- a/daemons/simulator/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# AVR Simulator - -`qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines. - -```bash -$ podman build -t dcc/net-to-serial:sim . -$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim -```