Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4a206af
Update Dockerfile
ShaYmez May 17, 2022
148b346
Update Dockerfile
ShaYmez Dec 15, 2022
ca222e1
Rename .gitignore to gitignoreHASH
ShaYmez Dec 28, 2022
407499a
Add workflow for docker-build
M0VUB Dec 28, 2022
5695a72
Update and rename gitignoreHASH to .gitignore
ShaYmez Dec 28, 2022
217a560
Update docker-compose.yml
ShaYmez Dec 28, 2022
36c9b1b
Update docker-compose.yml
ShaYmez Jan 4, 2023
236858c
Update Dockerfile
ShaYmez May 25, 2023
de42de5
Update Dockerfile
ShaYmez May 25, 2023
50d2a25
Update Dockerfile
ShaYmez Aug 5, 2023
1ae31ab
Create playback-SAMPLE.cfg
ShaYmez Aug 6, 2023
d09273e
Update hblink-SAMPLE.cfg
ShaYmez Aug 6, 2023
0f492ac
Update .gitignore
ShaYmez Aug 6, 2023
9ba9de8
Update rules_SAMPLE.py
ShaYmez Aug 6, 2023
34e1d38
Update docker-compose.yml
ShaYmez Aug 6, 2023
4cad03f
Update docker-compose.yml
ShaYmez Aug 6, 2023
ee789c8
Update entrypoint
ShaYmez Aug 6, 2023
37d5a4a
Update entrypoint
ShaYmez Aug 6, 2023
3c4f3b4
Rename playback-SAMPLE.cfg to playback.cfg
ShaYmez Aug 6, 2023
c7ebc03
Update hblink-SAMPLE.cfg
ShaYmez Aug 6, 2023
fd51df9
Update playback.cfg
ShaYmez Aug 6, 2023
2e0e044
Update hblink-SAMPLE.cfg
ShaYmez Aug 6, 2023
7b9fd4b
Update playback.cfg
ShaYmez Aug 6, 2023
ab94a12
Update docker-compose.yml
ShaYmez Aug 6, 2023
71c285d
Update docker-compose.yml
ShaYmez Aug 7, 2023
9f489e0
Update playback.cfg
ShaYmez Aug 11, 2023
4cf181c
Create version.txt
ShaYmez Jun 13, 2024
b99dc88
Update version.txt
ShaYmez Nov 13, 2024
1481b09
Initial plan
Copilot Dec 13, 2025
db045ad
Fix typos, remove commented code, and simplify boolean comparisons
Copilot Dec 13, 2025
f4db4a7
Merge pull request #1 from ShaYmez/copilot/analyse-and-review
ShaYmez Dec 13, 2025
866d222
Initial plan
Copilot Dec 13, 2025
c67c49f
Fix Debian 13 compatibility: Update Alpine base, fix shell script, up…
Copilot Dec 13, 2025
8e7c23d
Update dependency versions for better Debian 13 compatibility and rem…
Copilot Dec 13, 2025
bada062
Add CHANGELOG.md to document Debian 13 compatibility updates
Copilot Dec 13, 2025
08ddc0a
Merge pull request #2 from ShaYmez/copilot/update-debian-compatibility
ShaYmez Dec 13, 2025
d9ed74e
Refactor Dockerfile for improved build process
ShaYmez Dec 13, 2025
6c1d71b
Refactor entrypoint script for improved execution
ShaYmez Dec 13, 2025
07e6cc6
Initial plan
Copilot Dec 14, 2025
fca8cdd
Update version to 2.0.2 and finalize changelog for stable release
Copilot Dec 14, 2025
275d162
Merge pull request #3 from ShaYmez/copilot/ready-release-2-0-2
ShaYmez Dec 14, 2025
f917068
Refactor hblink3 service configuration in docker-compose
ShaYmez Dec 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Build-HBlink3

on:
push:
branches: master

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
- name: install buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: login to docker hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: build the image
run: |
docker buildx build --push \
--tag shaymez/hblink3:latest \
--platform linux/i386,linux/amd64,linux/arm64,linux/arm/v7 .
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__/
*.so

# Distribution / packaging
.github
.Python
env/
build/
Expand Down Expand Up @@ -98,3 +99,6 @@ local_subscriber_ids.*
peer_ids.*
local_peer_ids.*
talkgroup_ids.*

# Workflows
.github/workflows/
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.0.2] - 2025-12-14

### Changed
- Updated Dockerfile base image from Alpine 3.18 to Alpine 3.20 for better compatibility with modern systems
- Updated GitHub Actions workflow to use current versions (checkout@v4, docker/setup-buildx-action@v3, docker/login-action@v3)
- Modernized docker-compose.yml by removing obsolete version field
- Updated Python dependency minimum versions for Debian 13 and Python 3.11/3.12 compatibility:
- bitstring: 3.1.9 → 4.0.0
- bitarray: 2.3.5 → 2.8.0
- Twisted: 21.7.0 → 24.0.0
- configparser: 5.2.0 → 7.0.0

### Fixed
- Fixed POSIX compliance in entrypoint script (changed `==` to `=` for shell comparison)
- Added error handling to `cd` command in entrypoint script

### Security
- Updated all GitHub Actions to latest versions for improved security
- Verified no security vulnerabilities in updated dependencies

## [1.6.10] - Previous Release

### Note
- This changelog was added as part of Debian 13 compatibility updates
- For changes prior to this version, please refer to git commit history
41 changes: 29 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
FROM python:3.9-alpine
###############################################################################
# Copyright (C) 2024 Shane aka, ShaYmez <support@gb7nr.co.uk>
# Version 2.0.2
###############################################################################

COPY entrypoint /entrypoint
FROM python:alpine3.20

RUN adduser -D -u 54000 radio

WORKDIR /hblink3

# Install build dependencies
RUN apk add --no-cache git gcc musl-dev libffi-dev openssl-dev cargo

# Copy only requirements first for better layer caching
COPY requirements.txt .

RUN adduser -D -u 54000 radio && \
apk update && \
apk add git gcc musl-dev && \
cd /opt && \
git clone https://github.com/ShaYmez/hblink3 && \
cd /opt/hblink3 && \
pip install --no-cache-dir -r requirements.txt && \
apk del git gcc musl-dev && \
chown -R radio: /opt/hblink3
RUN pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt

# Remove build dependencies
RUN apk del git gcc musl-dev libffi-dev openssl-dev cargo

# Copy the application code
COPY . .

RUN chown -R radio /hblink3

COPY entrypoint /entrypoint
RUN chmod +x /entrypoint

USER radio

ENTRYPOINT [ "/entrypoint" ]
ENTRYPOINT ["/entrypoint"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Please join the DVSwitch group at groups.io for online forum support, discussion

DVSwitch@groups.io

A voluntary registrty for HBlink systems with public access has been created at http://hblink-register.com.es Please consider listing your system if you allow open access.
A voluntary registry for HBlink systems with public access has been created at http://hblink-register.com.es Please consider listing your system if you allow open access.

---

Expand Down
43 changes: 21 additions & 22 deletions bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
bridge will both receive traffic from, and send traffic to any other system
joined to the same conference bridge. It does not provide end-to-end connectivity
as each end system must individually be joined to a conference bridge (a name
you create in the configuraiton file) to pass traffic.
you create in the configuration file) to pass traffic.

This program currently only works with group voice calls.
'''
Expand All @@ -51,7 +51,6 @@

# Stuff for socket reporting
import pickle
# REMOVE LATER from datetime import datetime
# The module needs logging, but handlers, etc. are controlled by the parent
import logging
logger = logging.getLogger(__name__)
Expand All @@ -65,7 +64,7 @@
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'

# Module gobal varaibles
# Module global variables

# Dictionary for dynamically mapping unit (subscriber) to a system.
# This is for pruning unit-to-uint calls to not broadcast once the
Expand Down Expand Up @@ -114,7 +113,7 @@ def make_bridges(_rules):
for i, e in enumerate(_system['OFF']):
_system['OFF'][i] = bytes_3(_system['OFF'][i])
_system['TIMEOUT'] = _system['TIMEOUT']*60
if _system['ACTIVE'] == True:
if _system['ACTIVE']:
_system['TIMER'] = time() + _system['TIMEOUT']
else:
_system['TIMER'] = time()
Expand All @@ -130,24 +129,24 @@ def rule_timer_loop():
for _bridge in BRIDGES:
for _system in BRIDGES[_bridge]:
if _system['TO_TYPE'] == 'ON':
if _system['ACTIVE'] == True:
if _system['ACTIVE']:
if _system['TIMER'] < _now:
_system['ACTIVE'] = False
logger.info('(ROUTER) Conference Bridge TIMEOUT: DEACTIVATE System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
timeout_in = _system['TIMER'] - _now
logger.info('(ROUTER) Conference Bridge ACTIVE (ON timer running): System: %s Bridge: %s, TS: %s, TGID: %s, Timeout in: %.2fs,', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']), timeout_in)
elif _system['ACTIVE'] == False:
elif not _system['ACTIVE']:
logger.debug('(ROUTER) Conference Bridge INACTIVE (no change): System: %s Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
elif _system['TO_TYPE'] == 'OFF':
if _system['ACTIVE'] == False:
if not _system['ACTIVE']:
if _system['TIMER'] < _now:
_system['ACTIVE'] = True
logger.info('(ROUTER) Conference Bridge TIMEOUT: ACTIVATE System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
timeout_in = _system['TIMER'] - _now
logger.info('(ROUTER) Conference Bridge INACTIVE (OFF timer running): System: %s Bridge: %s, TS: %s, TGID: %s, Timeout in: %.2fs,', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']), timeout_in)
elif _system['ACTIVE'] == True:
elif _system['ACTIVE']:
logger.debug('(ROUTER) Conference Bridge ACTIVE (no change): System: %s Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
logger.debug('(ROUTER) Conference Bridge NO ACTION: System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
Expand Down Expand Up @@ -335,22 +334,22 @@ def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _
# The "continue" at the end of each means the next iteration of the for loop that tests for matching rules
#
if ((_target['TGID'] != _target_status[_target['TS']]['RX_TGID']) and ((pkt_time - _target_status[_target['TS']]['RX_TIME']) < _target_system['GROUP_HANGTIME'])):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID %s, target active or in group hangtime: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['RX_TGID']))
continue
if ((_target['TGID'] != _target_status[_target['TS']]['TX_TGID']) and ((pkt_time - _target_status[_target['TS']]['TX_TIME']) < _target_system['GROUP_HANGTIME'])):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID%s, target in group hangtime: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['TX_TGID']))
continue
if (_target['TGID'] == _target_status[_target['TS']]['RX_TGID']) and ((pkt_time - _target_status[_target['TS']]['RX_TIME']) < STREAM_TO):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID%s, matching call already active on target: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['RX_TGID']))
continue
if (_target['TGID'] == _target_status[_target['TS']]['TX_TGID']) and (_rf_src != _target_status[_target['TS']]['TX_RFS']) and ((pkt_time - _target_status[_target['TS']]['TX_TIME']) < STREAM_TO):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed for subscriber %s, call route in progress on target: HBSystem: %s, TS: %s, TGID: %s, SUB: %s', self._system, int_id(_rf_src), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['TX_TGID']), int_id(_target_status[_target['TS']]['TX_RFS']))
continue
Expand Down Expand Up @@ -512,23 +511,23 @@ def unit_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _d
#
'''
if ((_dst_id != _target_status[_slot]['RX_TGID']) and ((pkt_time - _target_status[_slot]['RX_TIME']) < _target_system['GROUP_HANGTIME'])):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID %s, target active or in group hangtime: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_dst_id), _target, _slot, int_id(_target_status[_slot]['RX_TGID']))
continue
if ((_dst_id != _target_status[_slot]['TX_TGID']) and ((pkt_time - _target_status[_slot]['TX_TIME']) < _target_system['GROUP_HANGTIME'])):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID%s, target in group hangtime: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_dst_id), _target, _slot, int_id(_target_status[_slot]['TX_TGID']))
continue
'''
if (_dst_id == _target_status[_slot]['RX_TGID']) and ((pkt_time - _target_status[_slot]['RX_TIME']) < STREAM_TO):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed to TGID%s, matching call already active on target: HBSystem: %s, TS: %s, TGID: %s', self._system, int_id(_dst_id), _target, _slot, int_id(_target_status[_slot]['RX_TGID']))
continue
if (_dst_id == _target_status[_slot]['TX_TGID']) and (_rf_src != _target_status[_slot]['TX_RFS']) and ((pkt_time - _target_status[_slot]['TX_TIME']) < STREAM_TO):
if self.STATUS[_stream_id]['CONTENTION'] == False:
if not self.STATUS[_stream_id]['CONTENTION']:
self.STATUS[_stream_id]['CONTENTION'] = True
logger.info('(%s) Call not routed for subscriber %s, call route in progress on target: HBSystem: %s, TS: %s, TGID: %s, SUB: %s', self._system, int_id(_rf_src), _target, _slot, int_id(_target_status[_slot]['TX_TGID']), int_id(_target_status[_slot]['TX_RFS']))
continue
Expand Down Expand Up @@ -847,15 +846,15 @@ def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _
if _system['SYSTEM'] == self._system:

# TGID matches a rule source, reset its timer
if _slot == _system['TS'] and _dst_id == _system['TGID'] and ((_system['TO_TYPE'] == 'ON' and (_system['ACTIVE'] == True)) or (_system['TO_TYPE'] == 'OFF' and _system['ACTIVE'] == False)):
if _slot == _system['TS'] and _dst_id == _system['TGID'] and ((_system['TO_TYPE'] == 'ON' and _system['ACTIVE']) or (_system['TO_TYPE'] == 'OFF' and not _system['ACTIVE'])):
_system['TIMER'] = pkt_time + _system['TIMEOUT']
logger.info('(%s) Transmission match for Bridge: %s. Reset timeout to %s', self._system, _bridge, _system['TIMER'])

# TGID matches an ACTIVATION trigger
if (_dst_id in _system['ON'] or _dst_id in _system['RESET']) and _slot == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_id in _system['ON']:
if _system['ACTIVE'] == False:
if not _system['ACTIVE']:
_system['ACTIVE'] = True
_system['TIMER'] = pkt_time + _system['TIMEOUT']
logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
Expand All @@ -864,27 +863,27 @@ def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _
_system['TIMER'] = pkt_time
logger.info('(%s) Bridge: %s set to "OFF" with an on timer rule: timeout timer cancelled', self._system, _bridge)
# Reset the timer for the rule
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON':
if _system['ACTIVE'] and _system['TO_TYPE'] == 'ON':
_system['TIMER'] = pkt_time + _system['TIMEOUT']
logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - pkt_time)

# TGID matches an DE-ACTIVATION trigger
if (_dst_id in _system['OFF'] or _dst_id in _system['RESET']) and _slot == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_id in _system['OFF']:
if _system['ACTIVE'] == True:
if _system['ACTIVE']:
_system['ACTIVE'] = False
logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
# Cancel the timer if we've enabled an "ON" type timeout
if _system['TO_TYPE'] == 'ON':
_system['TIMER'] = pkt_time
logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)
# Reset the timer for the rule
if _system['ACTIVE'] == False and _system['TO_TYPE'] == 'OFF':
if not _system['ACTIVE'] and _system['TO_TYPE'] == 'OFF':
_system['TIMER'] = pkt_time + _system['TIMEOUT']
logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - pkt_time)
# Cancel the timer if we've enabled an "ON" type timeout
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON' and _dst_group in _system['OFF']:
if _system['ACTIVE'] and _system['TO_TYPE'] == 'ON' and _dst_group in _system['OFF']:
_system['TIMER'] = pkt_time
logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)

Expand Down
2 changes: 1 addition & 1 deletion bridge_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
__email__ = 'n0mjs@me.com'
__status__ = 'pre-alpha'

# Module gobal varaibles
# Module global variables


class bridgeallSYSTEM(HBSYSTEM):
Expand Down
6 changes: 3 additions & 3 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

'''
This module generates the configuration data structure for hblink.py and
assoicated programs that use it. It has been seaparated into a different
module so as to keep hblink.py easeier to navigate. This file only needs
updated if the items in the main configuraiton file (usually hblink.cfg)
associated programs that use it. It has been separated into a different
module so as to keep hblink.py easier to navigate. This file only needs
updated if the items in the main configuration file (usually hblink.cfg)
change.
'''

Expand Down
31 changes: 18 additions & 13 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
version: '2.4'
services:
hblink3:
container_name: hblink
volumes:
- '/etc/hblink3/hblink.cfg:/opt/hblink3/hblink.cfg'
- '/var/log/hblink/hblink.log:/opt/hblink3/hblink.log'
- '/etc/hblink3/rules.py:/opt/hblink3/rules.py'
ports:
- '62030:62030/udp'
- '62031-62051:62031-62051/udp'
- '4321:4321/tcp'
image: 'shaymez/hblink3:latest'
restart: "unless-stopped"
hblink3:
container_name: hblink
volumes:
- '/etc/hblink3/hblink.cfg:/hblink3/hblink.cfg'
- '/var/log/hblink/hblink.log:/hblink3/hblink.log'
- '/etc/hblink3/rules.py:/hblink3/rules.py'
- '/etc/hblink3/json/:/hblink3/json/'
ports:
# Master Ports (99 Masters)
- '54000-54099:54000-54099/udp'
# MMDVM & OBP Ports
- '62030-62050:62030-62050/udp'
# TCP Port for HBmonitor
- '4321:4321/tcp'
image: 'shaymez/hblink3:latest'
restart: "unless-stopped"
environment:
- 'PARROT_ENABLE=1'
Loading