fix config.py, add editing of user settings to dashboard, limit number of stored locations per user

This commit is contained in:
KF7EEL 2021-03-11 07:52:06 -08:00
parent 247168d567
commit 4e0c87dac9
5 changed files with 206 additions and 49 deletions

View File

@ -252,7 +252,6 @@ def user_setting_write(dmr_id, setting, value):
logger.info('Current settings: ' + str(user_dict)) logger.info('Current settings: ' + str(user_dict))
if dmr_id not in user_dict: if dmr_id not in user_dict:
user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}] user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}]
if setting.upper() == 'ICON': if setting.upper() == 'ICON':
user_dict[dmr_id][2]['icon'] = value user_dict[dmr_id][2]['icon'] = value
if setting.upper() == 'SSID': if setting.upper() == 'SSID':
@ -261,6 +260,11 @@ def user_setting_write(dmr_id, setting, value):
user_comment = user_dict[dmr_id][3]['comment'] = value[0:35] user_comment = user_dict[dmr_id][3]['comment'] = value[0:35]
if setting.upper() == 'APRS': if setting.upper() == 'APRS':
user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}] user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}]
if setting.upper() == 'PIN':
try:
user_dict[dmr_id][4]['pin'] = value
except:
user_dict[dmr_id].append({'pin': value})
f.close() f.close()
logger.info('Loaded user settings. Preparing to write...') logger.info('Loaded user settings. Preparing to write...')
# Write modified dict to file # Write modified dict to file
@ -270,10 +274,6 @@ def user_setting_write(dmr_id, setting, value):
logger.info('User setting saved') logger.info('User setting saved')
f.close() f.close()
packet_assembly = '' packet_assembly = ''
## except:
## logger.info('No data file found, creating one.')
## #Path('./user_settings.txt').mkdir(parents=True, exist_ok=True)
## Path('./user_settings.txt').touch()
# Process SMS, do something bases on message # Process SMS, do something bases on message
@ -288,6 +288,8 @@ def process_sms(_rf_src, sms):
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@SSID| ','',sms)) user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@SSID| ','',sms))
elif '@COM' in sms: elif '@COM' in sms:
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@COM |@COM','',sms)) user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@COM |@COM','',sms))
elif '@PIN' in sms:
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), int(re.sub('@PIN |@PIN','',sms)))
# Write blank entry to cause APRS receive to look for packets for this station. # Write blank entry to cause APRS receive to look for packets for this station.
elif '@APRS' in sms: elif '@APRS' in sms:
user_setting_write(int_id(_rf_src), 'APRS', '') user_setting_write(int_id(_rf_src), 'APRS', '')

View File

@ -279,7 +279,6 @@ def build_config(_config_file):
CONFIG['SYSTEMS'].update({section: { CONFIG['SYSTEMS'].update({section: {
'MODE': config.get(section, 'MODE'), 'MODE': config.get(section, 'MODE'),
'ENABLED': config.getboolean(section, 'ENABLED'), 'ENABLED': config.getboolean(section, 'ENABLED'),
'APRS': config.getboolean(section, 'APRS'),
'REPEAT': config.getboolean(section, 'REPEAT'), 'REPEAT': config.getboolean(section, 'REPEAT'),
'MAX_PEERS': config.getint(section, 'MAX_PEERS'), 'MAX_PEERS': config.getint(section, 'MAX_PEERS'),
'IP': gethostbyname(config.get(section, 'IP')), 'IP': gethostbyname(config.get(section, 'IP')),

View File

@ -152,15 +152,41 @@ def aprs_send(packet):
AIS.sendall(packet) AIS.sendall(packet)
AIS.close() AIS.close()
logger.info('Packet sent to APRS-IS.') logger.info('Packet sent to APRS-IS.')
# For future use
##def position_timer(aprs_call):
## dash_entries = ast.literal_eval(os.popen('cat ' + loc_file).read())
## for i in dash_entries:
## if aprs_call == i['call']:
## if time.time()
def dashboard_loc_write(call, lat, lon, time, comment): def dashboard_loc_write(call, lat, lon, time, comment):
#try: #try:
dash_entries = ast.literal_eval(os.popen('cat ' + loc_file).read()) dash_entries = ast.literal_eval(os.popen('cat ' + loc_file).read())
# except: # except:
# dash_entries = [] # dash_entries = []
dash_entries.insert(0, {'call': call, 'lat': lat, 'lon': lon, 'time':time, 'comment': comment}) list_index = 0
call_count = 0
new_dash_entries = []
for i in dash_entries:
if i['call'] == call:
if call_count >= 25:
print(call_count)
pass
else:
new_dash_entries.append(i)
call_count = call_count + 1
if call != i['call']:
print('Record call: |' + i['call'] + '|')
print('Filter Call: |' + call + '|')
new_dash_entries.append(i)
pass
list_index = list_index + 1
with open(loc_file, 'w') as user_loc_file: with open(loc_file, 'w') as user_loc_file:
user_loc_file.write(str(dash_entries[:200])) user_loc_file.write(str(new_dash_entries[:500]))
user_loc_file.close() user_loc_file.close()
logger.info('User location saved for dashboard') logger.info('User location saved for dashboard')
#logger.info(dash_entries) #logger.info(dash_entries)
@ -245,7 +271,6 @@ def user_setting_write(dmr_id, setting, value):
logger.info('Current settings: ' + str(user_dict)) logger.info('Current settings: ' + str(user_dict))
if dmr_id not in user_dict: if dmr_id not in user_dict:
user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}] user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}]
if setting.upper() == 'ICON': if setting.upper() == 'ICON':
user_dict[dmr_id][2]['icon'] = value user_dict[dmr_id][2]['icon'] = value
if setting.upper() == 'SSID': if setting.upper() == 'SSID':
@ -254,6 +279,11 @@ def user_setting_write(dmr_id, setting, value):
user_comment = user_dict[dmr_id][3]['comment'] = value[0:35] user_comment = user_dict[dmr_id][3]['comment'] = value[0:35]
if setting.upper() == 'APRS': if setting.upper() == 'APRS':
user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}] user_dict[dmr_id] = [{'call': str(get_alias((dmr_id), subscriber_ids))}, {'ssid': ''}, {'icon': ''}, {'comment': ''}]
if setting.upper() == 'PIN':
try:
user_dict[dmr_id][4]['pin'] = value
except:
user_dict[dmr_id].append({'pin': value})
f.close() f.close()
logger.info('Loaded user settings. Preparing to write...') logger.info('Loaded user settings. Preparing to write...')
# Write modified dict to file # Write modified dict to file
@ -263,10 +293,6 @@ def user_setting_write(dmr_id, setting, value):
logger.info('User setting saved') logger.info('User setting saved')
f.close() f.close()
packet_assembly = '' packet_assembly = ''
## except:
## logger.info('No data file found, creating one.')
## #Path('./user_settings.txt').mkdir(parents=True, exist_ok=True)
## Path('./user_settings.txt').touch()
# Process SMS, do something bases on message # Process SMS, do something bases on message
@ -281,6 +307,8 @@ def process_sms(_rf_src, sms):
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@SSID| ','',sms)) user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@SSID| ','',sms))
elif '@COM' in sms: elif '@COM' in sms:
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@COM |@COM','',sms)) user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), re.sub('@COM |@COM','',sms))
elif '@PIN' in sms:
user_setting_write(int_id(_rf_src), re.sub(' .*|@','',sms), int(re.sub('@PIN |@PIN','',sms)))
# Write blank entry to cause APRS receive to look for packets for this station. # Write blank entry to cause APRS receive to look for packets for this station.
elif '@APRS' in sms: elif '@APRS' in sms:
user_setting_write(int_id(_rf_src), 'APRS', '') user_setting_write(int_id(_rf_src), 'APRS', '')

View File

@ -60,7 +60,7 @@ def get_loc_data():
<h2>&nbsp;<strong>Longitude</strong>&nbsp;</h2> <h2>&nbsp;<strong>Longitude</strong>&nbsp;</h2>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
<h2>&nbsp;<strong>Local Time</strong>&nbsp;</h2> <h2>&nbsp;<strong>Time</strong>&nbsp;</h2>
</td> </td>
</tr> </tr>
''' '''
@ -101,13 +101,13 @@ def get_bb_data():
<h2><strong>&nbsp;Callsign&nbsp;</strong></h2> <h2><strong>&nbsp;Callsign&nbsp;</strong></h2>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
<h2>&nbsp;<strong>DMR ID</strong>&nbsp; </h2> <h2>&nbsp;<strong>ID</strong>&nbsp; </h2>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
<h2>&nbsp;<strong>Bulletin</strong>&nbsp;</h2> <h2>&nbsp;<strong>Bulletin</strong>&nbsp;</h2>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
<h2>&nbsp;<strong>Local Time</strong>&nbsp;</h2> <h2>&nbsp;<strong>Time</strong>&nbsp;</h2>
</td> </td>
</tr> </tr>
''' '''
@ -178,6 +178,22 @@ def aprs_to_latlon(x):
minutes = x - 100*degrees minutes = x - 100*degrees
return degrees + minutes/60 return degrees + minutes/60
def user_setting_write(dmr_id, input_ssid, input_icon, input_comment):
dmr_id = int(dmr_id)
user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read())
new_dict = user_settings
new_dict[dmr_id][1]['ssid'] = input_ssid
new_dict[dmr_id][2]['icon'] = input_icon
new_dict[dmr_id][3]['comment'] = input_comment
print(input_comment)
print(new_dict[dmr_id])
# Write modified dict to file
with open(user_settings_file, 'w') as user_dict_file:
user_dict_file.write(str(new_dict))
user_dict_file.close()
@app.route('/') @app.route('/')
def index(): def index():
value = Markup('<strong>The HTML String</strong>') value = Markup('<strong>The HTML String</strong>')
@ -206,6 +222,7 @@ def view_map():
map_size = request.args.get('map_size') map_size = request.args.get('map_size')
user_loc = ast.literal_eval(os.popen('cat ' + loc_file).read()) user_loc = ast.literal_eval(os.popen('cat ' + loc_file).read())
last_known_list = [] last_known_list = []
coord_list = []
try: try:
if track_call: if track_call:
#folium_map = folium.Map(location=map_center, zoom_start=int(zoom_level)) #folium_map = folium.Map(location=map_center, zoom_start=int(zoom_level))
@ -320,6 +337,7 @@ def view_map():
if 'W' in user_coord['lon']: if 'W' in user_coord['lon']:
user_lon = -user_lon user_lon = -user_lon
loc_comment = '' loc_comment = ''
coord_list.append([user_lat, user_lon])
if 'comment' in user_coord: if 'comment' in user_coord:
loc_comment = """ loc_comment = """
<tr> <tr>
@ -351,18 +369,21 @@ def view_map():
</i>""", icon=folium.Icon(color="red", icon="record"), tooltip=str(user_coord['call'])).add_to(folium_map) </i>""", icon=folium.Icon(color="red", icon="record"), tooltip=str(user_coord['call'])).add_to(folium_map)
last_known_list.append(user_coord['call']) last_known_list.append(user_coord['call'])
if user_coord['call'] in last_known_list: if user_coord['call'] in last_known_list:
folium.CircleMarker([user_lat, user_lon], popup=""" if coord_list.count([user_lat, user_lon]) > 15:
<table style="width: 150px;"> pass
<tbody> else:
<tr> folium.CircleMarker([user_lat, user_lon], popup="""
<td style="text-align: center;"><strong>""" + user_coord['call'] + """</strong></td> <table style="width: 150px;">
</tr> <tbody>
<tr> <tr>
<td style="text-align: center;"><em>""" + loc_time + """</em></td> <td style="text-align: center;"><strong>""" + user_coord['call'] + """</strong></td>
</tr> </tr>
</tbody> <tr>
</table> <td style="text-align: center;"><em>""" + loc_time + """</em></td>
""", tooltip=str(user_coord['call']), fill=True, fill_color="#3186cc", radius=4).add_to(marker_cluster) </tr>
</tbody>
</table>
""", tooltip=str(user_coord['call']), fill=True, fill_color="#3186cc", radius=4).add_to(marker_cluster)
return folium_map._repr_html_() return folium_map._repr_html_()
@ -370,37 +391,136 @@ def view_map():
def map(): def map():
return render_template('map.html', title = dashboard_title, logo = logo) return render_template('map.html', title = dashboard_title, logo = logo)
@app.route('/user') @app.route('/user', methods = ['GET', 'POST'])
def user_settings(): def user_settings():
user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read())
user_id = request.args.get('user_id') user_id = request.args.get('user_id')
if not user_id: if request.method == 'POST' and request.form.get('dmr_id'):
user_result = """ if int(request.form.get('dmr_id')) in user_settings:
Use this tool to find and check the stored APRS settings for your DMR ID. When a position is sent, the stored settings will be used to format the APRS packet. user_id = request.form.get('dmr_id')
<form action="user" method="get"> ssid = user_settings[int(request.form.get('dmr_id'))][1]['ssid']
<table style="margin-left: auto; margin-right: auto;"> icon = user_settings[int(request.form.get('dmr_id'))][2]['icon']
comment = user_settings[int(request.form.get('dmr_id'))][3]['comment']
try:
pin = user_settings[int(request.form.get('dmr_id'))][4]['pin']
if ssid == '':
ssid = aprs_ssid
if icon == '':
icon = '\['
if comment == '':
comment = default_comment + ' ' + user_id
user_result = """
Use this tool to change the stored APRS settings for your DMR ID. When a position is sent, the stored settings will be used to format the APRS packet. Leave field(s) blank for default value.
<h2 style="text-align: center;">&nbsp;Modify Settings for ID: """ + user_id + """</h2>
<form action="user" method="post">
<table style="margin-left: auto; margin-right: auto; width: 419.367px;" border="1">
<tbody> <tbody>
<tr style="height: 62px;"> <tr>
<td style="text-align: center; height: 62px;"> <td style="width: 82px;"><strong>Callsign:</strong></td>
<h2><strong><label for="user_id">DMR ID:</label></strong></h2> <td style="width: 319.367px; text-align: center;"><strong>""" + str(user_settings[int(user_id)][0]['call']) + """</strong></td>
</td>
</tr> </tr>
<tr style="height: 51.1667px;"> <tr>
<td style="height: 51.1667px;"><input id="user_id" name="user_id" type="text" /></td> <td style="width: 82px;"><strong>SSID:</strong></td>
<td style="width: 319.367px; text-align: center;"><input id="ssid" name="ssid" type="text" placeholder='""" + ssid + """' /></td>
</tr> </tr>
<tr style="height: 27px;"> <tr>
<td style="text-align: center; height: 27px;"><input type="submit" value="Submit" /></td> <td style="width: 82px;"><strong>Icon:</strong></td>
<td style="width: 319.367px; text-align: center;"><input id="icon" name="icon" type="text" placeholder='""" + icon + """' /></td>
</tr>
<tr>
<td style="width: 82px;"><strong>Comment:</strong></td>
<td style="width: 319.367px; text-align: center;"><input id="comment" name="comment" type="text" placeholder='""" + comment + """'/></td>
</tr>
<tr>
<td style="width: 82px;"><strong>DMR ID:</strong></td>
<td style="width: 319.367px; text-align: center;"><input id="dmr_id" name="dmr_id" type="text" value='""" + user_id + """'/></td>
</tr>
<tr>
<td style="width: 82px;"><strong>PIN:</strong></td>
<td style="width: 319.367px; text-align: center;"><input id="pin" name="pin" type="password" /></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p style="text-align: center;"><input type="submit" value="Submit" /></p>
</form> </form>
<p>&nbsp;</p>
"""
except:
user_result = """<h2 style="text-align: center;">No PIN set for """ + str(user_settings[int(user_id)][0]['call']) + """ - """ + request.form.get('dmr_id') + """</h2>
<p style="text-align: center;"><button onclick="history.back()">Back</button>
</p>"""
if int(request.form.get('dmr_id')) not in user_settings:
user_result = """<h2 style="text-align: center;">DMR ID not found.</h2>
<p style="text-align: center;"><button onclick="history.back()">Back</button>
</p>"""
#if edit_user:
if request.method == 'POST' and request.form.get('dmr_id') and request.form.get('pin'):
if int(request.form.get('pin')) == pin:
ssid = request.form.get('ssid')
icon = request.form.get('icon')
comment = request.form.get('comment')
user_setting_write(request.form.get('dmr_id'), request.form.get('ssid'), request.form.get('icon'), request.form.get('comment'))
user_result = """<h2 style="text-align: center;">Changed settings for """ + str(user_settings[int(user_id)][0]['call']) + """ - """ + request.form.get('dmr_id') + """</h2>
<p style="text-align: center;"><button onclick="history.back()">Back</button>
</p>"""
if int(request.form.get('pin')) != pin:
user_result = """<h2 style="text-align: center;">Incorrect PIN.</h2>
<p style="text-align: center;"><button onclick="history.back()">Back</button>
</p>"""
if request.method == 'GET' and not request.args.get('user_id'):
user_result = """
Use this tool to find, check, and change the stored APRS settings for your DMR ID. When a position is sent, the stored settings will be used to format the APRS packet.
<table style="width: 600px; margin-left: auto; margin-right: auto;" border="3">
<tbody>
<tr>
<td><form action="user" method="get">
<table style="margin-left: auto; margin-right: auto;">
<tbody>
<tr style="height: 62px;">
<td style="text-align: center; height: 62px;">
<h2><strong><label for="user_id">Look up DMR ID:</label></strong></h2>
</td>
</tr>
<tr style="height: 51.1667px;">
<td style="height: 51.1667px;"><input id="user_id" name="user_id" type="text" /></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: center; height: 27px;"><input type="submit" value="Submit" /></td>
</tr>
</tbody>
</table>
</form></td>
<td><form action="user" method="post">
<table style="margin-left: auto; margin-right: auto;">
<tbody>
<tr style="height: 62px;">
<td style="text-align: center; height: 62px;">
<h2><strong><label for="dmr_id">Edit DMR ID:</label></strong></h2>
</td>
</tr>
<tr style="height: 51.1667px;">
<td style="height: 51.1667px;"><input id="dmr_id" name="dmr_id" type="text" /></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: center; height: 27px;"><input type="submit" value="Submit" /></td>
</tr>
</tbody>
</table>
</form></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p> <p>&nbsp;</p>
""" """
else: #else:
if request.method == 'GET' and request.args.get('user_id'):
try: try:
#return render_template('map.html', title = dashboard_title, logo = logo)
user_settings = ast.literal_eval(os.popen('cat ../../user_settings.txt').read())
call = user_settings[int(user_id)][0]['call'] call = user_settings[int(user_id)][0]['call']
ssid = user_settings[int(user_id)][1]['ssid'] ssid = user_settings[int(user_id)][1]['ssid']
icon = user_settings[int(user_id)][2]['icon'] icon = user_settings[int(user_id)][2]['icon']
@ -452,7 +572,7 @@ def mailbox():
if not recipient: if not recipient:
mail_content = """ mail_content = """
<p>The Mailbox is a place where users can leave messages via DMR SMS. A user can leave a message for someone else by sending a specially formatted SMS to <strong>""" + data_call_id + """</strong>. <p>The Mailbox is a place where users can leave messages via DMR SMS. A user can leave a message for someone else by sending a specially formatted SMS to <strong>""" + data_call_id + """</strong>.
The message recipient can then use the mailbox to check for messages. You can also check for APRS mesages addressed to your DMR radio. Enter your call sign below to check for messages. See the <a href="help">help</a> page for more information.</p> The message recipient can then use the mailbox to check for messages. You can also check for APRS mesages addressed to your DMR radio. Enter your call sign (without APRS SSID) below to check for messages. See the <a href="help">help</a> page for more information.</p>
<form action="mailbox" method="get"> <form action="mailbox" method="get">
<table style="margin-left: auto; margin-right: auto;"> <table style="margin-left: auto; margin-right: auto;">
<tbody> <tbody>
@ -544,7 +664,8 @@ def bb_rss():
<title>""" + entry['call'] + ' - ' + str(entry['dmr_id']) + """</title> <title>""" + entry['call'] + ' - ' + str(entry['dmr_id']) + """</title>
<link>""" + rss_link + """</link> <link>""" + rss_link + """</link>
<description>""" + entry['bulletin'] + """ - """ + loc_time + """</description> <description>""" + entry['bulletin'] + """ - """ + loc_time + """</description>
</item> <pubDate>""" + datetime.fromtimestamp(entry['time']).strftime('%a, %d %b %y') +"""</pubDate>
</item>
""" """
return Response(rss_header + post_data + "\n</channel>\n</rss>", mimetype='text/xml') return Response(rss_header + post_data + "\n</channel>\n</rss>", mimetype='text/xml')
except Exception as e: except Exception as e:
@ -573,6 +694,7 @@ def mail_rss():
<title>""" + entry['call'] + ' - ' + str(entry['dmr_id']) + """</title> <title>""" + entry['call'] + ' - ' + str(entry['dmr_id']) + """</title>
<link>""" + rss_link + """</link> <link>""" + rss_link + """</link>
<description>""" + entry['message'] + """ - """ + loc_time + """</description> <description>""" + entry['message'] + """ - """ + loc_time + """</description>
<pubDate>""" + datetime.fromtimestamp(entry['time']).strftime('%a, %d %b %y') +"""</pubDate>
</item> </item>
""" """
return Response(rss_header + post_data + "\n</channel>\n</rss>", mimetype='text/xml') return Response(rss_header + post_data + "\n</channel>\n</rss>", mimetype='text/xml')
@ -636,6 +758,7 @@ if __name__ == '__main__':
loc_file = parser.get('GPS_DATA', 'LOCATION_FILE') loc_file = parser.get('GPS_DATA', 'LOCATION_FILE')
emergency_sos_file = parser.get('GPS_DATA', 'EMERGENCY_SOS_FILE') emergency_sos_file = parser.get('GPS_DATA', 'EMERGENCY_SOS_FILE')
the_mailbox_file = parser.get('GPS_DATA', 'MAILBOX_FILE') the_mailbox_file = parser.get('GPS_DATA', 'MAILBOX_FILE')
user_settings_file = parser.get('GPS_DATA', 'USER_SETTINGS_FILE')
######################## ########################
app.run(debug = True, port=dash_port, host=dash_host) app.run(debug = True, port=dash_port, host=dash_host)

View File

@ -51,6 +51,11 @@
<td><em><code>@SSID 7</code></em></td> <td><em><code>@SSID 7</code></em></td>
</tr> </tr>
<tr> <tr>
<td><strong>@PIN</strong></td>
<td>Set a PIN. This is used for changing your APRS settings ia the dashboard. You must set this for each DMR ID you wish to change via the dashboard.</td>
<td><em><code>@PIN 1234</code></em></td>
</tr>
<tr>
<td><strong>@MH</strong></td> <td><strong>@MH</strong></td>
<td>Set you location by maidenhead grid square. Designed for radios with no GPS or that are not compatable yet.</td> <td>Set you location by maidenhead grid square. Designed for radios with no GPS or that are not compatable yet.</td>
<td><em><code>@MH DN97uk</code></em></td> <td><em><code>@MH DN97uk</code></em></td>