Skip to content

Commit 26a2523

Browse files
authored
Merge pull request #8 from Ddoiron-cidco/master
add support for dbt and dpt and add unit test and ci
2 parents 5ec26ef + cc5e011 commit 26a2523

File tree

13 files changed

+448
-141
lines changed

13 files changed

+448
-141
lines changed

.github/workflows/tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
unit-tests:
9+
name: Unit tests (Python ${{ matrix.python-version }})
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install pyserial
29+
30+
- name: Run unit tests
31+
run: bash Script/unittest.sh

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
nbproject
22
nbproject/*
33
build
4-
build/*
4+
build/*
5+
__pycache__/
6+
*.pyc

Install/install.sh

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1-
sudo apt install python3 python-is-python3 python3-pip -y
1+
#!/usr/bin/env bash
2+
set -euo pipefail
23

3-
sudo bash -c 'cat << EOF3 > /etc/systemd/system/nmea.service
4+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
6+
sudo apt update
7+
sudo apt install -y python3 python3-pip python3-serial
8+
9+
sudo tee /etc/systemd/system/nmea.service >/dev/null <<EOF
410
[Unit]
511
Description=Launch NMEA Simulator on boot.
12+
After=network-online.target
13+
Wants=network-online.target
614
715
[Service]
816
Type=simple
9-
ExecStart=/home/ubuntu/SonarSimulator/Script/autolaunch.sh
17+
WorkingDirectory=${PROJECT_ROOT}
18+
Environment=SONAR_SERIAL_PORT=/dev/ttyUSB0
19+
Environment=SONAR_BAUD_RATE=9600
20+
Environment=SONAR_SENTENCE_TYPE=dpt
21+
ExecStart=${PROJECT_ROOT}/Script/autolaunch.sh
22+
Restart=always
23+
RestartSec=2
1024
1125
[Install]
1226
WantedBy=multi-user.target
13-
EOF3'
27+
EOF
1428

15-
sudo chmod 755 /etc/systemd/system/nmea.service
29+
sudo chmod 644 /etc/systemd/system/nmea.service
30+
sudo systemctl daemon-reload
1631
sudo systemctl enable nmea
17-
sudo systemctl start nmea
18-
sudo systemctl status nmea
19-
32+
sudo systemctl restart nmea
33+
sudo systemctl --no-pager --full status nmea

README.md

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,82 @@
1-
# README for nmeaSIMULATOR.py
2-
## Sonar simulator NMEA-0183
1+
# SonarSimulator
32

3+
NMEA-0183 depth simulator for serial-connected systems.
44

5+
## Features
6+
- Generates `SDDPT` or `SDDBT` frames once per second by default.
7+
- Uses a deterministic depth pattern based on current second (`10 + second`).
8+
- Sends data over a serial port (`/dev/ttyUSB0` and `9600` by default).
9+
- Can run manually or as a `systemd` service (`nmea.service`).
510

6-
### foncitonnalities :
11+
## Repository Layout
12+
- `Script/sonar_simulator.py`: simulator entrypoint.
13+
- `Script/autolaunch.sh`: launcher used by systemd.
14+
- `Script/sonar_simulator_unittest.py`: unit tests.
15+
- `Script/unittest.sh`: test runner.
16+
- `Install/install.sh`: service installation script.
17+
- `BOM.txt`: hardware bill of materials.
718

8-
Generate nmea-0183 string of types :
9-
- Depth of water (SDDPT)
19+
## Requirements
20+
- Ubuntu (tested workflow targets Raspberry Pi with Ubuntu Server).
21+
- Python 3.
22+
- `pyserial` (`python3-serial` package).
1023

11-
### HOW TO MAKE IT WORK:
12-
13-
On a Raspberry pi :
24+
## Manual Run
25+
From repository root:
1426

15-
1. Install ubuntu server on as sdcard with the user ubuntu
27+
```bash
28+
python3 Script/sonar_simulator.py /dev/ttyUSB0 9600
29+
```
1630

17-
2. Connect the usb serial adapter on the Raspberry
31+
Optional arguments:
1832

19-
3. apply the power on the raspberry
33+
```bash
34+
python3 Script/sonar_simulator.py /dev/ttyUSB0 9600 --sentence-type dbt --interval 0.5 --iterations 10
35+
```
2036

21-
4. validate if the serial adapter is detected by the OS
22-
ls /dev/ttyU*
23-
you should see ttyUSB0
24-
25-
5. clone the repository in /home/ubuntu
37+
## Install as Service
38+
From repository root:
2639

27-
6. Validate and modify the setting for the serial port and baudrate in the autolaunch.sh
28-
nano /home/ubuntu/SonarSimulator/autolaunch.sh
29-
python /home/ubuntu/SonarSimulator/Script/sonar_simulator_DPT.py /dev/ttyUSB0 9600
30-
the 2 last argument are the serial port and the baudrate
31-
ex:
32-
python /home/ubuntu/SonarSimulator/Script/sonar_simulator_DPT.py /dev/ttyUSB1 9600
33-
python /home/ubuntu/SonarSimulator/Script/sonar_simulator_DPT.py /dev/ttyAMA0 115200
40+
```bash
41+
bash Install/install.sh
42+
```
3443

35-
7. go in the install directory and run the installation script
36-
cd /home/ubuntu/SonarSimulator/Install
37-
./install.sh
44+
This installs and starts `nmea.service` with default environment values:
45+
- `SONAR_SERIAL_PORT=/dev/ttyUSB0`
46+
- `SONAR_BAUD_RATE=9600`
47+
- `SONAR_SENTENCE_TYPE=dpt`
3848

39-
The script is installed as a service.
40-
If you do some modification in the autolaunch.sh file you have to relaunch the service.
49+
Service commands:
50+
51+
```bash
52+
sudo systemctl status nmea
4153
sudo systemctl restart nmea
54+
sudo systemctl stop nmea
55+
```
56+
57+
## Change Serial Port, Baud Rate, or Sentence Type
58+
Option 1 (recommended): override service environment values in the unit file.
59+
60+
```bash
61+
sudo systemctl edit --full nmea
62+
# Update SONAR_SERIAL_PORT, SONAR_BAUD_RATE, and SONAR_SENTENCE_TYPE (dpt or dbt)
63+
sudo systemctl daemon-reload
64+
sudo systemctl restart nmea
65+
```
66+
67+
Option 2: export environment values before running manually.
4268

43-
to show the status of the service : sudo systemctl status nmea
44-
to start the service : sudo systemctl start nmea
45-
to stop the service : sudo systemctl stop nmea
69+
```bash
70+
SONAR_SERIAL_PORT=/dev/ttyUSB1 SONAR_BAUD_RATE=115200 SONAR_SENTENCE_TYPE=dbt bash Script/autolaunch.sh
71+
```
4672

73+
## Run Tests
74+
From repository root:
4775

76+
```bash
77+
bash Script/unittest.sh
78+
```
4879

80+
## Notes
81+
- The simulator writes one frame per cycle terminated by `\r\n`.
82+
- If serial opening fails, the script logs the error and exits cleanly.

Script/DPT_unittest.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

Script/autolaunch.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1-
#!/bin/bash
1+
#!/usr/bin/env bash
2+
set -euo pipefail
23

3-
python /home/ubuntu/SonarSimulator/Script/sonar_simulator_DPT.py /dev/ttyUSB0 9600
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
SERIAL_PORT="${SONAR_SERIAL_PORT:-/dev/ttyUSB0}"
6+
BAUD_RATE="${SONAR_BAUD_RATE:-9600}"
7+
SENTENCE_TYPE="${SONAR_SENTENCE_TYPE:-dpt}"
8+
9+
exec python3 "${SCRIPT_DIR}/sonar_simulator.py" \
10+
"${SERIAL_PORT}" \
11+
"${BAUD_RATE}" \
12+
--sentence-type "${SENTENCE_TYPE}"

Script/sonar_simulator.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import argparse
2+
import datetime
3+
import time
4+
5+
import serial
6+
7+
8+
def generate_nmea_dpt(depth):
9+
return checksum(f"SDDPT,{depth:.1f},0.1,100")
10+
11+
12+
def generate_nmea_dbt(depth):
13+
feet = depth * 3.28084
14+
fathoms = depth * 0.546806649
15+
return checksum(f"SDDBT,{feet:.1f},f,{depth:.1f},M,{fathoms:.1f},F")
16+
17+
18+
def generate_nmea_sentence(depth, sentence_type):
19+
normalized_type = sentence_type.lower()
20+
if normalized_type == "dpt":
21+
return generate_nmea_dpt(depth)
22+
if normalized_type == "dbt":
23+
return generate_nmea_dbt(depth)
24+
raise ValueError(f"Unsupported sentence type: {sentence_type}")
25+
26+
27+
def checksum(sentence):
28+
checksum_value = 0
29+
for char in sentence:
30+
checksum_value ^= ord(char)
31+
return f"${sentence}*{checksum_value:02X}"
32+
33+
34+
def depth_from_now(now=None):
35+
if now is None:
36+
now = datetime.datetime.now()
37+
return 10 + now.second
38+
39+
40+
def main(
41+
serial_port,
42+
baud_rate,
43+
sentence_type="dpt",
44+
iterations=None,
45+
interval=1.0,
46+
now_provider=None,
47+
sleep_func=time.sleep,
48+
):
49+
if now_provider is None:
50+
now_provider = datetime.datetime.now
51+
52+
ser = None
53+
try:
54+
ser = serial.Serial(serial_port, baud_rate, timeout=1)
55+
print(f"Connected to serial port {serial_port} at {baud_rate} baud.")
56+
57+
sent_count = 0
58+
while iterations is None or sent_count < iterations:
59+
depth = depth_from_now(now_provider())
60+
nmea_sentence = generate_nmea_sentence(depth, sentence_type)
61+
ser.write((nmea_sentence + "\r\n").encode("ascii"))
62+
print(nmea_sentence)
63+
sent_count += 1
64+
65+
if iterations is None or sent_count < iterations:
66+
sleep_func(interval)
67+
68+
except KeyboardInterrupt:
69+
print("Script stopped by user.")
70+
except Exception as exc:
71+
print(f"An error occurred: {exc}")
72+
finally:
73+
if ser is not None and ser.is_open:
74+
ser.close()
75+
76+
77+
def parse_args():
78+
parser = argparse.ArgumentParser(description="NMEA-0183 sonar simulator (DPT/DBT)")
79+
parser.add_argument("serial_port", help="Serial device path (example: /dev/ttyUSB0)")
80+
parser.add_argument("baud_rate", type=int, help="Serial baud rate (example: 9600)")
81+
parser.add_argument(
82+
"--sentence-type",
83+
choices=["dpt", "dbt"],
84+
default="dpt",
85+
help="NMEA sentence type to emit (default: dpt)",
86+
)
87+
parser.add_argument(
88+
"--interval",
89+
type=float,
90+
default=1.0,
91+
help="Delay between sentences in seconds (default: 1.0)",
92+
)
93+
parser.add_argument(
94+
"--iterations",
95+
type=int,
96+
default=None,
97+
help="Number of sentences to send (default: infinite loop)",
98+
)
99+
return parser.parse_args()
100+
101+
102+
if __name__ == "__main__":
103+
args = parse_args()
104+
main(
105+
serial_port=args.serial_port,
106+
baud_rate=args.baud_rate,
107+
sentence_type=args.sentence_type,
108+
iterations=args.iterations,
109+
interval=args.interval,
110+
)

0 commit comments

Comments
 (0)